Browse Source

Refactored DrawingContext and VisualBrush, added DrawingBrush (#10419)

Refactored DrawingContext and VisualBrush, added DrawingBrush
pull/10462/head
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
ae1fcfed51
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  2. 5
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  3. 4
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  4. 66
      src/Avalonia.Base/Media/DrawingBrush.cs
  5. 403
      src/Avalonia.Base/Media/DrawingContext.cs
  6. 139
      src/Avalonia.Base/Media/DrawingGroup.cs
  7. 2
      src/Avalonia.Base/Media/DrawingImage.cs
  8. 31
      src/Avalonia.Base/Media/ISceneBrush.cs
  9. 16
      src/Avalonia.Base/Media/IVisualBrush.cs
  10. 2
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  11. 18
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  12. 6
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  13. 66
      src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
  14. 112
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  15. 24
      src/Avalonia.Base/Media/VisualBrush.cs
  16. 6
      src/Avalonia.Base/Platform/IRenderTarget.cs
  17. 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  18. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  19. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  20. 183
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  21. 30
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  22. 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  23. 29
      src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
  24. 37
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  25. 15
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  26. 4
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  27. 39
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  28. 10
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  29. 37
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  30. 24
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  31. 4
      src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
  32. 30
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  33. 33
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  34. 10
      src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
  35. 21
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  36. 19
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  37. 41
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  38. 52
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  39. 7
      src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
  40. 2
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  41. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  42. 4
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  43. 2
      src/Avalonia.X11/X11CursorFactory.cs
  44. 2
      src/Avalonia.X11/X11IconLoader.cs
  45. 136
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  46. 3
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  47. 3
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  48. 3
      src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs
  49. 10
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  50. 55
      src/Skia/Avalonia.Skia/PictureRenderTarget.cs
  51. 3
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  52. 4
      src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs
  53. 8
      src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs
  54. 32
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  55. 2
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  56. 4
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  57. 8
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  58. 4
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  59. 4
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  60. 2
      tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs
  61. 4
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  62. 4
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs
  63. 2
      tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
  64. 4
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  65. 94
      tests/Avalonia.RenderTests/Media/TileBrushTests.cs
  66. 6
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  67. 4
      tests/Avalonia.UnitTests/TestRoot.cs
  68. BIN
      tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  69. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  70. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png

20
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
@ -8,22 +9,27 @@ using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia; using Avalonia.Skia;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
using SkiaSharp; using SkiaSharp;
namespace RenderDemo.Pages namespace RenderDemo.Pages
{ {
public class CustomSkiaPage : Control public class CustomSkiaPage : Control
{ {
private readonly GlyphRun _noSkia;
public CustomSkiaPage() public CustomSkiaPage()
{ {
ClipToBounds = true; ClipToBounds = true;
var text = "Current rendering API is not Skia";
var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
_noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
} }
class CustomDrawOp : ICustomDrawOperation class CustomDrawOp : ICustomDrawOperation
{ {
private readonly FormattedText _noSkia; private readonly GlyphRun _noSkia;
public CustomDrawOp(Rect bounds, FormattedText noSkia) public CustomDrawOp(Rect bounds, GlyphRun noSkia)
{ {
_noSkia = noSkia; _noSkia = noSkia;
Bounds = bounds; Bounds = bounds;
@ -42,10 +48,7 @@ namespace RenderDemo.Pages
{ {
var leaseFeature = context.GetFeature<ISkiaSharpApiLeaseFeature>(); var leaseFeature = context.GetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature == null) if (leaseFeature == null)
using (var c = new DrawingContext(context, false)) context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl);
{
c.DrawText(_noSkia, new Point());
}
else else
{ {
using var lease = leaseFeature.Lease(); using var lease = leaseFeature.Lease();
@ -114,10 +117,7 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture, context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia));
FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
} }
} }

5
samples/RenderDemo/Pages/PathMeasurementPage.cs

@ -37,11 +37,8 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
using (var ctxi = _bitmap.CreateDrawingContext(null)) using (var bitmapCtx = _bitmap.CreateDrawingContext())
using (var bitmapCtx = new DrawingContext(ctxi, false))
{ {
ctxi.Clear(default);
var basePath = new PathGeometry(); var basePath = new PathGeometry();
using (var basePathCtx = basePath.Open()) using (var basePathCtx = basePath.Open())

4
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -28,13 +28,11 @@ namespace RenderDemo.Pages
readonly Stopwatch _st = Stopwatch.StartNew(); readonly Stopwatch _st = Stopwatch.StartNew();
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
using (var ctxi = _bitmap.CreateDrawingContext(null)) using (var ctx = _bitmap.CreateDrawingContext())
using(var ctx = new DrawingContext(ctxi, false))
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100))) * Matrix.CreateTranslation(100, 100)))
{ {
ctxi.Clear(default);
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
} }

66
src/Avalonia.Base/Media/DrawingBrush.cs

@ -0,0 +1,66 @@
using Avalonia.Media.Immutable;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="Drawing"/>.
/// </summary>
public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender
{
/// <summary>
/// Defines the <see cref="Drawing"/> property.
/// </summary>
public static readonly StyledProperty<Drawing?> DrawingProperty =
AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing));
static DrawingBrush()
{
AffectsRender<DrawingBrush>(DrawingProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary>
public DrawingBrush()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary>
/// <param name="visual">The visual to draw.</param>
public DrawingBrush(Drawing visual)
{
Drawing = visual;
}
/// <summary>
/// Gets or sets the visual to draw.
/// </summary>
public Drawing? Drawing
{
get { return GetValue(DrawingProperty); }
set { SetValue(DrawingProperty, value); }
}
ISceneBrushContent? ISceneBrush.CreateContent()
{
if (Drawing == null)
return null;
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
Drawing?.Draw(recorder);
var drawList = recorder.EndUpdate();
if (drawList == null)
return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
drawList.CalculateBounds(), true);
}
}
}

403
src/Avalonia.Base/Media/DrawingContext.cs

@ -8,83 +8,45 @@ using Avalonia.Media.Imaging;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class DrawingContext : IDisposable public abstract class DrawingContext : IDisposable
{ {
private readonly bool _ownsImpl; private static ThreadSafeObjectPool<Stack<RestoreState>> StateStackPool { get; } =
private int _currentLevel; ThreadSafeObjectPool<Stack<RestoreState>>.Default;
private Stack<RestoreState>? _states;
private static ThreadSafeObjectPool<Stack<PushedState>> StateStackPool { get; } = internal DrawingContext()
ThreadSafeObjectPool<Stack<PushedState>>.Default;
private static ThreadSafeObjectPool<Stack<TransformContainer>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<TransformContainer>>.Default;
private Stack<PushedState>? _states = StateStackPool.Get();
private Stack<TransformContainer>? _transformContainers = TransformStackPool.Get();
readonly struct TransformContainer
{
public readonly Matrix LocalTransform;
public readonly Matrix ContainerTransform;
public TransformContainer(Matrix localTransform, Matrix containerTransform)
{
LocalTransform = localTransform;
ContainerTransform = containerTransform;
}
}
public DrawingContext(IDrawingContextImpl impl)
{ {
PlatformImpl = impl;
_ownsImpl = true;
} }
public DrawingContext(IDrawingContextImpl impl, bool ownsImpl)
{
_ownsImpl = ownsImpl;
PlatformImpl = impl;
}
public IDrawingContextImpl PlatformImpl { get; }
private Matrix _currentTransform = Matrix.Identity;
private Matrix _currentContainerTransform = Matrix.Identity; public void Dispose()
/// <summary>
/// Gets the current transform of the drawing context.
/// </summary>
public Matrix CurrentTransform
{ {
get { return _currentTransform; } if (_states != null)
private set
{ {
_currentTransform = value; while (_states.Count > 0)
var transform = _currentTransform * _currentContainerTransform; _states.Pop().Dispose();
PlatformImpl.Transform = transform;
}
}
//HACK: This is a temporary hack that is used in the render loop StateStackPool.ReturnAndSetNull(ref _states);
//to update TransformedBounds property }
[Obsolete("HACK for render loop, don't use")]
public Matrix CurrentContainerTransform => _currentContainerTransform;
DisposeCore();
}
protected abstract void DisposeCore();
/// <summary> /// <summary>
/// Draws an image. /// Draws an image.
/// </summary> /// </summary>
/// <param name="source">The image.</param> /// <param name="source">The image.</param>
/// <param name="rect">The rect in the output to draw to.</param> /// <param name="rect">The rect in the output to draw to.</param>
public void DrawImage(IImage source, Rect rect) public virtual void DrawImage(IImage source, Rect rect)
{ {
_ = source ?? throw new ArgumentNullException(nameof(source)); _ = source ?? throw new ArgumentNullException(nameof(source));
DrawImage(source, new Rect(source.Size), rect); DrawImage(source, new Rect(source.Size), rect);
} }
/// <summary> /// <summary>
/// Draws an image. /// Draws an image.
/// </summary> /// </summary>
@ -92,12 +54,22 @@ namespace Avalonia.Media
/// <param name="sourceRect">The rect in the image to draw.</param> /// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param> /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = default)
{ {
_ = source ?? throw new ArgumentNullException(nameof(source)); _ = source ?? throw new ArgumentNullException(nameof(source));
source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
} }
/// <summary>
/// Draws a platform-specific bitmap impl.
/// </summary>
/// <param name="source">The bitmap image.</param>
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
internal abstract void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
/// <summary> /// <summary>
/// Draws a line. /// Draws a line.
@ -108,11 +80,11 @@ namespace Avalonia.Media
public void DrawLine(IPen pen, Point p1, Point p2) public void DrawLine(IPen pen, Point p1, Point p2)
{ {
if (PenIsVisible(pen)) if (PenIsVisible(pen))
{ DrawLineCore(pen, p1, p2);
PlatformImpl.DrawLine(pen, p1, p2);
}
} }
protected abstract void DrawLineCore(IPen pen, Point p1, Point p2);
/// <summary> /// <summary>
/// Draws a geometry. /// Draws a geometry.
/// </summary> /// </summary>
@ -121,10 +93,10 @@ namespace Avalonia.Media
/// <param name="geometry">The geometry.</param> /// <param name="geometry">The geometry.</param>
public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry)
{ {
if (geometry.PlatformImpl is not null) if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null)
DrawGeometry(brush, pen, geometry.PlatformImpl); DrawGeometryCore(brush, pen, geometry.PlatformImpl);
} }
/// <summary> /// <summary>
/// Draws a geometry. /// Draws a geometry.
/// </summary> /// </summary>
@ -133,14 +105,12 @@ namespace Avalonia.Media
/// <param name="geometry">The geometry.</param> /// <param name="geometry">The geometry.</param>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{ {
_ = geometry ?? throw new ArgumentNullException(nameof(geometry)); if ((brush != null || PenIsVisible(pen)))
DrawGeometryCore(brush, pen, geometry);
if (brush != null || PenIsVisible(pen))
{
PlatformImpl.DrawGeometry(brush, pen, geometry);
}
} }
protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry);
/// <summary> /// <summary>
/// Draws a rectangle with the specified Brush and Pen. /// Draws a rectangle with the specified Brush and Pen.
/// </summary> /// </summary>
@ -158,14 +128,12 @@ namespace Avalonia.Media
/// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks> /// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect,
double radiusX = 0, double radiusY = 0,
BoxShadows boxShadows = default) BoxShadows boxShadows = default)
{ {
if (brush == null && !PenIsVisible(pen)) if (brush == null && !PenIsVisible(pen))
{
return; return;
}
if (!MathUtilities.IsZero(radiusX)) if (!MathUtilities.IsZero(radiusX))
{ {
radiusX = Math.Min(radiusX, rect.Width / 2); radiusX = Math.Min(radiusX, rect.Width / 2);
@ -175,20 +143,48 @@ namespace Avalonia.Media
{ {
radiusY = Math.Min(radiusY, rect.Height / 2); radiusY = Math.Min(radiusY, rect.Height / 2);
} }
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
}
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rrect">The rectangle bounds.</param>
/// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
return;
DrawRectangleCore(brush, pen, rrect, boxShadows);
} }
protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
BoxShadows boxShadows = default);
/// <summary> /// <summary>
/// Draws the outline of a rectangle. /// Draws the outline of a rectangle.
/// </summary> /// </summary>
/// <param name="pen">The pen.</param> /// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param> /// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param> /// <param name="cornerRadius">The corner radius.</param>
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) =>
{
DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); DrawRectangle(null, pen, rect, cornerRadius, cornerRadius);
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) =>
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
/// <summary> /// <summary>
/// Draws an ellipse with the specified Brush and Pen. /// Draws an ellipse with the specified Brush and Pen.
@ -204,35 +200,50 @@ namespace Avalonia.Media
/// </remarks> /// </remarks>
public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY)
{ {
if (brush == null && !PenIsVisible(pen)) if (brush != null || PenIsVisible(pen))
{ {
return; var originX = center.X - radiusX;
var originY = center.Y - radiusY;
var width = radiusX * 2;
var height = radiusY * 2;
DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height));
} }
}
var originX = center.X - radiusX;
var originY = center.Y - radiusY; /// <summary>
var width = radiusX * 2; /// Draws an ellipse with the specified Brush and Pen.
var height = radiusY * 2; /// </summary>
/// <param name="brush">The brush used to fill the ellipse, or <c>null</c> for no fill.</param>
PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); /// <param name="pen">The pen used to stroke the ellipse, or <c>null</c> for no stroke.</param>
/// <param name="rect">The bounding rect.</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if (brush != null || PenIsVisible(pen))
DrawEllipseCore(brush, pen, rect);
} }
protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect);
/// <summary> /// <summary>
/// Draws a custom drawing operation /// Draws a custom drawing operation
/// </summary> /// </summary>
/// <param name="custom">custom operation</param> /// <param name="custom">custom operation</param>
public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); public abstract void Custom(ICustomDrawOperation custom);
/// <summary> /// <summary>
/// Draws text. /// Draws text.
/// </summary> /// </summary>
/// <param name="origin">The upper-left corner of the text.</param> /// <param name="origin">The upper-left corner of the text.</param>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
public void DrawText(FormattedText text, Point origin) public virtual void DrawText(FormattedText text, Point origin)
{ {
_ = text ?? throw new ArgumentNullException(nameof(text)); _ = text ?? throw new ArgumentNullException(nameof(text));
text.Draw(this, origin); text.Draw(this, origin);
} }
/// <summary> /// <summary>
@ -240,30 +251,31 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="foreground">The foreground brush.</param> /// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param> /// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun);
public record struct PushedState : IDisposable
{ {
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); private readonly DrawingContext _context;
private readonly int _level;
if (foreground != null) public PushedState(DrawingContext context)
{ {
PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); _context = context;
_level = _context._states!.Count;
} }
}
/// <summary> public void Dispose()
/// Draws a filled rectangle. {
/// </summary> if(_context?._states == null)
/// <param name="brush">The brush.</param> return;
/// <param name="rect">The rectangle bounds.</param> if(_context._states.Count != _level)
/// <param name="cornerRadius">The corner radius.</param> throw new InvalidOperationException("Wrong Push/Pop state order");
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) _context._states.Pop().Dispose();
{ }
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
} }
public readonly record struct PushedState : IDisposable private readonly record struct RestoreState : IDisposable
{ {
private readonly int _level;
private readonly DrawingContext _context; private readonly DrawingContext _context;
private readonly Matrix _matrix; private readonly Matrix _matrix;
private readonly PushedStateType _type; private readonly PushedStateType _type;
@ -271,62 +283,56 @@ namespace Avalonia.Media
public enum PushedStateType public enum PushedStateType
{ {
None, None,
Matrix, Transform,
Opacity, Opacity,
Clip, Clip,
MatrixContainer,
GeometryClip, GeometryClip,
OpacityMask, OpacityMask,
BitmapBlendMode
} }
public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default) public RestoreState(DrawingContext context, PushedStateType type)
{ {
if (context._states is null)
throw new ObjectDisposedException(nameof(DrawingContext));
_context = context; _context = context;
_type = type; _type = type;
_matrix = matrix;
_level = context._currentLevel += 1;
context._states.Push(this);
} }
public void Dispose() public void Dispose()
{ {
if (_type == PushedStateType.None) if (_type == PushedStateType.None)
return; return;
if (_context._states is null || _context._transformContainers is null) if (_context._states is null)
throw new ObjectDisposedException(nameof(DrawingContext)); throw new ObjectDisposedException(nameof(DrawingContext));
if (_context._currentLevel != _level) if (_type == PushedStateType.Transform)
throw new InvalidOperationException("Wrong Push/Pop state order"); _context.PopTransformCore();
_context._currentLevel--;
_context._states.Pop();
if (_type == PushedStateType.Matrix)
_context.CurrentTransform = _matrix;
else if (_type == PushedStateType.Clip) else if (_type == PushedStateType.Clip)
_context.PlatformImpl.PopClip(); _context.PopClipCore();
else if (_type == PushedStateType.Opacity) else if (_type == PushedStateType.Opacity)
_context.PlatformImpl.PopOpacity(); _context.PopOpacityCore();
else if (_type == PushedStateType.GeometryClip) else if (_type == PushedStateType.GeometryClip)
_context.PlatformImpl.PopGeometryClip(); _context.PopGeometryClipCore();
else if (_type == PushedStateType.OpacityMask) else if (_type == PushedStateType.OpacityMask)
_context.PlatformImpl.PopOpacityMask(); _context.PopOpacityMaskCore();
else if (_type == PushedStateType.MatrixContainer) else if (_type == PushedStateType.BitmapBlendMode)
{ _context.PopBitmapBlendModeCore();
var cont = _context._transformContainers.Pop();
_context._currentContainerTransform = cont.ContainerTransform;
_context.CurrentTransform = cont.LocalTransform;
}
} }
} }
/// <summary>
/// Pushes a clip rectangle.
/// </summary>
/// <param name="clip">The clip rectangle.</param>
/// <returns>A disposable used to undo the clip rectangle.</returns>
public PushedState PushClip(RoundedRect clip) public PushedState PushClip(RoundedRect clip)
{ {
PlatformImpl.PushClip(clip); PushClipCore(clip);
return new PushedState(this, PushedState.PushedStateType.Clip); _states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
return new PushedState(this);
} }
protected abstract void PushClipCore(RoundedRect rect);
/// <summary> /// <summary>
/// Pushes a clip rectangle. /// Pushes a clip rectangle.
/// </summary> /// </summary>
@ -334,9 +340,13 @@ namespace Avalonia.Media
/// <returns>A disposable used to undo the clip rectangle.</returns> /// <returns>A disposable used to undo the clip rectangle.</returns>
public PushedState PushClip(Rect clip) public PushedState PushClip(Rect clip)
{ {
PlatformImpl.PushClip(clip); PushClipCore(clip);
return new PushedState(this, PushedState.PushedStateType.Clip); _states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
return new PushedState(this);
} }
protected abstract void PushClipCore(Rect rect);
/// <summary> /// <summary>
/// Pushes a clip geometry. /// Pushes a clip geometry.
@ -345,17 +355,13 @@ namespace Avalonia.Media
/// <returns>A disposable used to undo the clip geometry.</returns> /// <returns>A disposable used to undo the clip geometry.</returns>
public PushedState PushGeometryClip(Geometry clip) public PushedState PushGeometryClip(Geometry clip)
{ {
_ = clip ?? throw new ArgumentNullException(nameof(clip)); PushGeometryClipCore(clip);
_states ??= StateStackPool.Get();
// HACK: This check was added when nullable annotations pointed out that we're potentially _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip));
// pushing a null value for the clip here. Ideally we'd return an empty PushedState here but return new PushedState(this);
// I don't want to make that change as part of adding nullable annotations.
if (clip.PlatformImpl is null)
throw new InvalidOperationException("Cannot push empty geometry clip.");
PlatformImpl.PushGeometryClip(clip.PlatformImpl);
return new PushedState(this, PushedState.PushedStateType.GeometryClip);
} }
protected abstract void PushGeometryClipCore(Geometry clip);
/// <summary> /// <summary>
/// Pushes an opacity value. /// Pushes an opacity value.
@ -364,11 +370,13 @@ namespace Avalonia.Media
/// <param name="bounds">The bounds.</param> /// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns> /// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity, Rect bounds) public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{ {
PlatformImpl.PushOpacity(opacity, bounds); PushOpacityCore(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity); _states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity));
return new PushedState(this);
} }
protected abstract void PushOpacityCore(double opacity, Rect bounds);
/// <summary> /// <summary>
/// Pushes an opacity mask. /// Pushes an opacity mask.
@ -380,70 +388,53 @@ namespace Avalonia.Media
/// <returns>A disposable to undo the opacity mask.</returns> /// <returns>A disposable to undo the opacity mask.</returns>
public PushedState PushOpacityMask(IBrush mask, Rect bounds) public PushedState PushOpacityMask(IBrush mask, Rect bounds)
{ {
PlatformImpl.PushOpacityMask(mask, bounds); PushOpacityMaskCore(mask, bounds);
return new PushedState(this, PushedState.PushedStateType.OpacityMask); _states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask));
return new PushedState(this);
} }
protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds);
/// <summary> public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode)
/// Pushes a matrix post-transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix);
/// <summary>
/// Pushes a matrix pre-transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform);
/// <summary>
/// Sets the current matrix transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushSetTransform(Matrix matrix)
{ {
var oldMatrix = CurrentTransform; PushBitmapBlendMode(blendingMode);
CurrentTransform = matrix; _states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode));
return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); return new PushedState(this);
} }
/// <summary> protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode);
/// Pushes a new transform context.
/// </summary>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushTransformContainer()
{
if (_transformContainers is null)
throw new ObjectDisposedException(nameof(DrawingContext));
_transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform));
_currentContainerTransform = CurrentTransform * _currentContainerTransform;
_currentTransform = Matrix.Identity;
return new PushedState(this, PushedState.PushedStateType.MatrixContainer);
}
/// <summary> /// <summary>
/// Disposes of any resources held by the <see cref="DrawingContext"/>. /// Pushes a matrix transformation.
/// </summary> /// </summary>
public void Dispose() /// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushTransform(Matrix matrix)
{ {
if (_states is null || _transformContainers is null) PushTransformCore(matrix);
throw new ObjectDisposedException(nameof(DrawingContext)); _states ??= StateStackPool.Get();
while (_states.Count != 0) _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform));
_states.Peek().Dispose(); return new PushedState(this);
StateStackPool.Return(_states);
_states = null;
if (_transformContainers.Count != 0)
throw new InvalidOperationException("Transform container stack is non-empty");
TransformStackPool.Return(_transformContainers);
_transformContainers = null;
if (_ownsImpl)
PlatformImpl.Dispose();
} }
[Obsolete("Use PushTransform")]
public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform")]
public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform")]
public PushedState PushTransformContainer() => PushTransform(Matrix.Identity);
protected abstract void PushTransformCore(Matrix matrix);
protected abstract void PopClipCore();
protected abstract void PopGeometryClipCore();
protected abstract void PopOpacityCore();
protected abstract void PopOpacityMaskCore();
protected abstract void PopBitmapBlendModeCore();
protected abstract void PopTransformCore();
private static bool PenIsVisible(IPen? pen) private static bool PenIsVisible(IPen? pen)
{ {
return pen?.Brush != null && pen.Thickness > 0; return pen?.Brush != null && pen.Thickness > 0;

139
src/Avalonia.Base/Media/DrawingGroup.cs

@ -67,10 +67,7 @@ namespace Avalonia.Media
} }
} }
public DrawingContext Open() public DrawingContext Open() => new DrawingGroupDrawingContext(this);
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public override void Draw(DrawingContext context) public override void Draw(DrawingContext context)
{ {
@ -105,7 +102,7 @@ namespace Avalonia.Media
return rect; return rect;
} }
private class DrawingGroupDrawingContext : IDrawingContextImpl private sealed class DrawingGroupDrawingContext : DrawingContext
{ {
private readonly DrawingGroup _drawingGroup; private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
@ -135,17 +132,7 @@ namespace Avalonia.Media
_drawingGroup = drawingGroup; _drawingGroup = drawingGroup;
} }
public Matrix Transform protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
get => _transform;
set
{
_transform = value;
PushTransform(new MatrixTransform(value));
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{ {
if ((brush == null) && (pen == null)) if ((brush == null) && (pen == null))
{ {
@ -159,7 +146,7 @@ namespace Avalonia.Media
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
} }
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{ {
if ((brush == null) && (pen == null)) if ((brush == null) && (pen == null))
{ {
@ -169,7 +156,7 @@ namespace Avalonia.Media
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
} }
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun) public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{ {
if (foreground == null) if (foreground == null)
{ {
@ -179,124 +166,70 @@ namespace Avalonia.Media
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{ {
Foreground = foreground, Foreground = foreground,
GlyphRun = new GlyphRun(glyphRun) GlyphRun = glyphRun
}; };
// Add Drawing to the Drawing graph // Add Drawing to the Drawing graph
AddDrawing(glyphRunDrawing); AddDrawing(glyphRunDrawing);
} }
public void DrawLine(IPen? pen, Point p1, Point p2) protected override void PushClipCore(RoundedRect rect)
{
if (pen == null)
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void Clear(Color color)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IDrawingContextLayerImpl CreateLayer(Size size) protected override void PushClipCore(Rect rect)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void Custom(ICustomDrawOperation custom) protected override void PushGeometryClipCore(Geometry clip)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public object? GetFeature(Type t) => null; protected override void PushOpacityCore(double opacity, Rect bounds)
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void PopBitmapBlendMode() protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void PopClip() internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void PopGeometryClip() protected override void DrawLineCore(IPen pen, Point p1, Point p2)
{ {
throw new NotImplementedException(); // Instantiate the geometry
} var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
public void PopOpacity()
{
throw new NotImplementedException();
}
public void PopOpacityMask()
{
throw new NotImplementedException();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
public void PushClip(Rect clip)
{
throw new NotImplementedException();
}
public void PushClip(RoundedRect clip) // Add Drawing to the Drawing graph
{ AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
throw new NotImplementedException();
} }
public void PushGeometryClip(IGeometryImpl clip) protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{ {
throw new NotImplementedException(); // Instantiate the geometry
} var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect);
public void PushOpacity(double opacity, Rect bounds) // Add Drawing to the Drawing graph
{ AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
throw new NotImplementedException();
} }
public void PushOpacityMask(IBrush mask, Rect bounds) public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException();
{
throw new NotImplementedException();
}
public void Dispose() protected override void DisposeCore()
{ {
// Dispose may be called multiple times without throwing // Dispose may be called multiple times without throwing
// an exception. // an exception.
@ -366,22 +299,34 @@ namespace Avalonia.Media
// Restore the previous value of the current drawing group // Restore the previous value of the current drawing group
_currentDrawingGroup = _previousDrawingGroupStack.Pop(); _currentDrawingGroup = _previousDrawingGroupStack.Pop();
} }
/// <summary> /// <summary>
/// PushTransform - /// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding /// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop. /// Pop.
/// </summary> /// </summary>
/// <param name="transform"> The Transform to push. </param> /// <param name="matrix"> The transform to push. </param>
private void PushTransform(Transform transform) protected override void PushTransformCore(Matrix matrix)
{ {
// Instantiate a new drawing group and set it as the _currentDrawingGroup // Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup(); var drawingGroup = PushNewDrawingGroup();
// Set the transform on the new DrawingGroup // Set the transform on the new DrawingGroup
drawingGroup.Transform = transform; drawingGroup.Transform = new MatrixTransform(matrix);
} }
protected override void PopClipCore() => Pop();
protected override void PopGeometryClipCore() => Pop();
protected override void PopOpacityCore() => Pop();
protected override void PopOpacityMaskCore() => Pop();
protected override void PopBitmapBlendModeCore() => Pop();
protected override void PopTransformCore() => Pop();
/// <summary> /// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the /// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup, /// _currentDrawingGroup to a newly instantiated DrawingGroup,

2
src/Avalonia.Base/Media/DrawingImage.cs

@ -62,7 +62,7 @@ namespace Avalonia.Media
-sourceRect.Y + destRect.Y - bounds.Y); -sourceRect.Y + destRect.Y - bounds.Y);
using (context.PushClip(destRect)) using (context.PushClip(destRect))
using (context.PushPreTransform(translate * scale)) using (context.PushTransform(translate * scale))
{ {
Drawing?.Draw(context); Drawing?.Draw(context);
} }

31
src/Avalonia.Base/Media/ISceneBrush.cs

@ -0,0 +1,31 @@
using System;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media
{
[NotClientImplementable]
public interface ISceneBrush : ITileBrush
{
ISceneBrushContent? CreateContent();
}
[NotClientImplementable]
public interface ISceneBrushContent : IImmutableBrush, IDisposable
{
ITileBrush Brush { get; }
Rect Rect { get; }
void Render(IDrawingContextImpl context, Matrix? transform);
internal bool UseScalableRasterization { get; }
}
internal class ImmutableSceneBrush : ImmutableTileBrush
{
public ImmutableSceneBrush(ITileBrush source) : base(source)
{
}
}
}

16
src/Avalonia.Base/Media/IVisualBrush.cs

@ -1,16 +0,0 @@
using Avalonia.Metadata;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
[NotClientImplementable]
public interface IVisualBrush : ITileBrush
{
/// <summary>
/// Gets the visual to draw.
/// </summary>
Visual? Visual { get; }
}
}

2
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging
Rect destRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode) BitmapInterpolationMode bitmapInterpolationMode)
{ {
context.PlatformImpl.DrawBitmap( context.DrawBitmap(
PlatformImpl, PlatformImpl,
1, 1,
sourceRect, sourceRect,

18
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging
/// <summary> /// <summary>
/// A bitmap that holds the rendering of a <see cref="Visual"/>. /// A bitmap that holds the rendering of a <see cref="Visual"/>.
/// </summary> /// </summary>
public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget public class RenderTargetBitmap : Bitmap, IDisposable
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class. /// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging
/// Renders a visual to the <see cref="RenderTargetBitmap"/>. /// Renders a visual to the <see cref="RenderTargetBitmap"/>.
/// </summary> /// </summary>
/// <param name="visual">The visual to render.</param> /// <param name="visual">The visual to render.</param>
public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); public void Render(Visual visual)
{
using (var ctx = CreateDrawingContext())
ImmediateRenderer.Render(visual, ctx);
}
/// <summary> /// <summary>
/// Creates a platform-specific implementation for a <see cref="RenderTargetBitmap"/>. /// Creates a platform-specific implementation for a <see cref="RenderTargetBitmap"/>.
@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging
return factory.CreateRenderTargetBitmap(size, dpi); return factory.CreateRenderTargetBitmap(size, dpi);
} }
/// <inheritdoc/> public DrawingContext CreateDrawingContext()
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); {
var platform = PlatformImpl.Item.CreateDrawingContext();
bool IRenderTarget.IsCorrupted => false; platform.Clear(Colors.Transparent);
return new PlatformDrawingContext(platform);
}
} }
} }

6
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -354,12 +354,10 @@ namespace Avalonia.Media
throw new ObjectDisposedException(nameof(DrawingContext)); throw new ObjectDisposedException(nameof(DrawingContext));
while (_states.Count != 0) while (_states.Count != 0)
_states.Peek().Dispose(); _states.Peek().Dispose();
StateStackPool.Return(_states); StateStackPool.ReturnAndSetNull(ref _states);
_states = null;
if (_transformContainers.Count != 0) if (_transformContainers.Count != 0)
throw new InvalidOperationException("Transform container stack is non-empty"); throw new InvalidOperationException("Transform container stack is non-empty");
TransformStackPool.Return(_transformContainers); TransformStackPool.ReturnAndSetNull(ref _transformContainers);
_transformContainers = null;
if (_ownsImpl) if (_ownsImpl)
PlatformImpl.Dispose(); PlatformImpl.Dispose();
} }

66
src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs

@ -1,66 +0,0 @@
using Avalonia.Media.Imaging;
namespace Avalonia.Media.Immutable
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush
{
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableImageBrush"/> class.
/// </summary>
/// <param name="visual">The visual to draw.</param>
/// <param name="alignmentX">The horizontal alignment of a tile in the destination.</param>
/// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
/// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
/// <param name="opacity">The opacity of the brush.</param>
/// <param name="transform">The transform of the brush.</param>
/// <param name="transformOrigin">The transform origin of the brush</param>
/// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
/// <param name="stretch">
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
public ImmutableVisualBrush(
Visual? visual,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
double opacity = 1,
ImmutableTransform? transform = null,
RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base(
alignmentX,
alignmentY,
destinationRect ?? RelativeRect.Fill,
opacity,
transform,
transformOrigin,
sourceRect ?? RelativeRect.Fill,
stretch,
tileMode,
bitmapInterpolationMode)
{
Visual = visual;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableVisualBrush"/> class.
/// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param>
public ImmutableVisualBrush(IVisualBrush source)
: base(source)
{
Visual = source.Visual;
}
/// <inheritdoc/>
public Visual? Visual { get; }
}
}

112
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media;
internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private readonly IDrawingContextImpl _impl;
private readonly bool _ownsImpl;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
private Stack<Matrix>? _transforms;
public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true)
{
_impl = impl;
_ownsImpl = ownsImpl;
}
protected override void DrawLineCore(IPen pen, Point p1, Point p2) =>
_impl.DrawLine(pen, p1, p2);
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) =>
_impl.DrawGeometry(brush, pen, geometry);
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
BoxShadows boxShadows = default) =>
_impl.DrawRectangle(brush, pen, rrect, boxShadows);
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect);
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) =>
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
public override void Custom(ICustomDrawOperation custom) =>
custom.Render(_impl);
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
if (foreground != null)
_impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
}
protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect);
protected override void PushClipCore(Rect rect) => _impl.PushClip(rect);
protected override void PushGeometryClipCore(Geometry clip) =>
_impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException());
protected override void PushOpacityCore(double opacity, Rect bounds) =>
_impl.PushOpacity(opacity, bounds);
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) =>
_impl.PushOpacityMask(mask, bounds);
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) =>
_impl.PushBitmapBlendMode(blendingMode);
protected override void PushTransformCore(Matrix matrix)
{
_transforms ??= TransformStackPool.Get();
var current = _impl.Transform;
_transforms.Push(current);
_impl.Transform = matrix * current;
}
protected override void PopClipCore() => _impl.PopClip();
protected override void PopGeometryClipCore() => _impl.PopGeometryClip();
protected override void PopOpacityCore() => _impl.PopOpacity();
protected override void PopOpacityMaskCore() => _impl.PopOpacityMask();
protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode();
protected override void PopTransformCore() =>
_impl.Transform =
(_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();
protected override void DisposeCore()
{
if (_ownsImpl)
_impl.Dispose();
if (_transforms != null)
{
if (_transforms.Count != 0)
throw new InvalidOperationException("Not all states are disposed");
TransformStackPool.ReturnAndSetNull(ref _transforms);
}
}
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
if (_impl is IDrawingContextWithAcrylicLikeSupport idc)
idc.DrawRectangle(material, rect);
else
DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
}
}

24
src/Avalonia.Base/Media/VisualBrush.cs

@ -1,11 +1,14 @@
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Paints an area with an <see cref="Visual"/>. /// Paints an area with an <see cref="Visual"/>.
/// </summary> /// </summary>
public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Visual"/> property. /// Defines the <see cref="Visual"/> property.
@ -43,10 +46,23 @@ namespace Avalonia.Media
set { SetValue(VisualProperty, value); } set { SetValue(VisualProperty, value); }
} }
/// <inheritdoc/> ISceneBrushContent? ISceneBrush.CreateContent()
IImmutableBrush IMutableBrush.ToImmutable()
{ {
return new ImmutableVisualBrush(this); if (Visual == null)
return null;
if (Visual is IVisualBrushInitialize initialize)
initialize.EnsureInitialized();
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
var drawList = recorder.EndUpdate();
if (drawList == null)
return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
new(Visual.Bounds.Size), false);
} }
} }
} }

6
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -14,11 +14,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session. /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary> /// </summary>
/// <param name="visualBrushRenderer"> IDrawingContextImpl CreateDrawingContext();
/// A render to be used to render visual brushes. May be null if no visual brushes are
/// to be drawn.
/// </param>
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
/// <summary> /// <summary>
/// Indicates if the render target is no longer usable and needs to be recreated /// Indicates if the render target is no longer usable and needs to be recreated

2
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor
{ {
_root = root; _root = root;
_compositor = compositor; _compositor = compositor;
_recordingContext = new DrawingContext(_recorder); _recordingContext = _recorder;
CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update; _update = Update;

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Collections.Pooled; using Avalonia.Collections.Pooled;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing;
/// </summary> /// </summary>
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>> internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
{ {
public Size? Size { get; set; }
public CompositionDrawList() public CompositionDrawList()
{ {
@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
public CompositionDrawList Clone() public CompositionDrawList Clone()
{ {
var clone = new CompositionDrawList(Count) { Size = Size }; var clone = new CompositionDrawList(Count);
foreach (var r in this) foreach (var r in this)
clone.Add(r.Clone()); clone.Add(r.Clone());
return clone; return clone;
} }
public void Render(CompositorDrawingContextProxy canvas) public void Render(IDrawingContextImpl canvas)
{
foreach (var cmd in this)
{
if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform;
cmd.Item.Render(canvas);
}
}
public void Render(IDrawingContextImpl canvas, Matrix transform)
{ {
foreach (var cmd in this) foreach (var cmd in this)
{ {
canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform * transform;
cmd.Item.Render(canvas); cmd.Item.Render(canvas);
} }
}
canvas.VisualBrushDrawList = null; public Rect CalculateBounds()
{
var rect = default(Rect);
foreach (var cmd in this)
rect = rect.Union(cmd.Item.Bounds);
return rect;
}
public bool HitTest(Point pt)
{
foreach (var op in this)
if (op.Item.HitTest(pt))
return true;
return false;
} }
} }

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs

@ -0,0 +1,37 @@
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositionDrawListSceneBrushContent : ISceneBrushContent
{
private readonly CompositionDrawList _drawList;
public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization)
{
Brush = brush;
Rect = rect;
UseScalableRasterization = useScalableRasterization;
_drawList = drawList;
}
public ITileBrush Brush { get; }
public Rect Rect { get; }
public double Opacity => Brush.Opacity;
public ITransform? Transform => Brush.Transform;
public RelativePoint TransformOrigin => Brush.TransformOrigin;
public void Dispose() => _drawList.Dispose();
public void Render(IDrawingContextImpl context, Matrix? transform)
{
if (transform.HasValue)
_drawList.Render(context, transform.Value);
else
_drawList.Render(context);
}
public bool UseScalableRasterization { get; }
}

183
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
@ -7,7 +8,7 @@ using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.Threading;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition;
/// <summary> /// <summary>
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/> /// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
/// </summary> /// </summary>
internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{ {
private CompositionDrawListBuilder _builder = new(); private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex; private int _drawOperationIndex;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
/// <inheritdoc/> private Stack<Matrix>? _transforms;
public Matrix Transform { get; set; } = Matrix.Identity;
/// <inheritdoc/> private static ThreadSafeObjectPool<Stack<bool>> OpacityMaskPopStackPool { get; } =
public void Clear(Color color) ThreadSafeObjectPool<Stack<bool>>.Default;
{
// Cannot clear a deferred scene.
}
/// <inheritdoc/> private Stack<bool>? _needsToPopOpacityMask;
public void Dispose()
{
// Nothing to do here since we allocate no unmanaged resources.
}
public Matrix Transform { get; set; } = Matrix.Identity;
public void BeginUpdate(CompositionDrawList? list) public void BeginUpdate(CompositionDrawList? list)
{ {
_builder.Reset(list); _builder.Reset(list);
_drawOperationIndex = 0; _drawOperationIndex = 0;
} }
public CompositionDrawList EndUpdate() public CompositionDrawList? EndUpdate()
{ {
// Make sure that any pending pop operations are completed
Dispose();
_builder.TrimTo(_drawOperationIndex); _builder.TrimTo(_drawOperationIndex);
return _builder.DrawOperations!; return _builder.DrawOperations;
} }
protected override void DisposeCore()
{
if (_transforms != null)
{
_transforms.Clear();
TransformStackPool.ReturnAndSetNull(ref _transforms);
}
/// <inheritdoc/> if (_needsToPopOpacityMask != null)
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) {
_needsToPopOpacityMask.Clear();
_needsToPopOpacityMask = null;
}
}
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{ {
var next = NextDrawAs<GeometryNode>(); var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{ {
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry));
} }
else else
{ {
@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
BitmapInterpolationMode bitmapInterpolationMode)
{ {
var next = NextDrawAs<ImageNode>(); var next = NextDrawAs<ImageNode>();
@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
/// <inheritdoc/> /// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(IPen? pen, Point p1, Point p2)
{ {
if (pen is null) if (pen is null)
{ {
@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{ {
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); Add(new LineNode(Transform, pen, p1, p2));
} }
else else
{ {
@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
/// <inheritdoc/> /// <inheritdoc/>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default) BoxShadows boxShadows = default)
{ {
var next = NextDrawAs<RectangleNode>(); var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{ {
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows));
} }
else else
{ {
@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{ {
var next = NextDrawAs<EllipseNode>(); var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{ {
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect));
} }
else else
{ {
++_drawOperationIndex; ++_drawOperationIndex;
} }
} }
public void Custom(ICustomDrawOperation custom) public override void Custom(ICustomDrawOperation custom)
{ {
var next = NextDrawAs<CustomDrawOperation>(); var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom)) if (next == null || !next.Item.Equals(Transform, custom))
@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
++_drawOperationIndex; ++_drawOperationIndex;
} }
public object? GetFeature(Type t) => null; public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
/// <inheritdoc/>
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
{ {
if (foreground is null) if (foreground is null)
{ {
@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
var next = NextDrawAs<GlyphRunNode>(); var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl))
{ {
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl));
} }
else else
@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
public IDrawingContextLayerImpl CreateLayer(Size size) protected override void PushTransformCore(Matrix matrix)
{ {
throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); _transforms ??= TransformStackPool.Get();
_transforms.Push(Transform);
Transform = matrix * Transform;
} }
protected override void PopTransformCore() =>
Transform = (_transforms ?? throw new InvalidOperationException()).Pop();
/// <inheritdoc/> protected override void PopClipCore()
public void PopClip()
{ {
var next = NextDrawAs<ClipNode>(); var next = NextDrawAs<ClipNode>();
@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
/// <inheritdoc/> /// <inheritdoc/>
public void PopGeometryClip() protected override void PopGeometryClipCore()
{ {
var next = NextDrawAs<GeometryClipNode>(); var next = NextDrawAs<GeometryClipNode>();
@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> protected override void PopBitmapBlendModeCore()
public void PopBitmapBlendMode()
{ {
var next = NextDrawAs<BitmapBlendModeNode>(); var next = NextDrawAs<BitmapBlendModeNode>();
@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> protected override void PopOpacityCore()
public void PopOpacity()
{ {
var next = NextDrawAs<OpacityNode>(); var next = NextDrawAs<OpacityNode>();
@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> protected override void PopOpacityMaskCore()
public void PopOpacityMask()
{ {
if (!_needsToPopOpacityMask!.Pop())
return;
var next = NextDrawAs<OpacityMaskNode>(); var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null)) if (next == null || !next.Item.Equals(null, null))
{ {
Add(new OpacityMaskNode()); Add(new OpacityMaskPopNode());
} }
else else
{ {
@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/>
public void PushClip(Rect clip) protected override void PushClipCore(Rect clip)
{ {
var next = NextDrawAs<ClipNode>(); var next = NextDrawAs<ClipNode>();
@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc /> protected override void PushClipCore(RoundedRect clip)
public void PushClip(RoundedRect clip)
{ {
var next = NextDrawAs<ClipNode>(); var next = NextDrawAs<ClipNode>();
@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> protected override void PushGeometryClipCore(Geometry clip)
public void PushGeometryClip(IGeometryImpl? clip)
{ {
if (clip is null) if (clip.PlatformImpl is null)
return; return;
var next = NextDrawAs<GeometryClipNode>(); var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(Transform, clip)) if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl))
{ {
Add(new GeometryClipNode(Transform, clip)); Add(new GeometryClipNode(Transform, clip.PlatformImpl));
} }
else else
{ {
++_drawOperationIndex; ++_drawOperationIndex;
} }
} }
/// <inheritdoc/> protected override void PushOpacityCore(double opacity, Rect bounds)
public void PushOpacity(double opacity, Rect bounds)
{ {
var next = NextDrawAs<OpacityNode>(); var next = NextDrawAs<OpacityNode>();
@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
} }
} }
/// <inheritdoc/> protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
public void PushOpacityMask(IBrush mask, Rect bounds)
{ {
var next = NextDrawAs<OpacityMaskNode>(); var next = NextDrawAs<OpacityMaskNode>();
bool needsToPop = true;
if (next == null || !next.Item.Equals(mask, bounds)) if (next == null || !next.Item.Equals(mask, bounds))
{ {
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); var immutableMask = ConvertBrush(mask);
if (immutableMask != null)
Add(new OpacityMaskNode(immutableMask, bounds));
else
needsToPop = false;
} }
else else
{ {
++_drawOperationIndex; ++_drawOperationIndex;
} }
_needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get();
_needsToPopOpacityMask.Push(needsToPop);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{ {
var next = NextDrawAs<BitmapBlendModeNode>(); var next = NextDrawAs<BitmapBlendModeNode>();
@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
: null; : null;
} }
private static IDisposable? CreateChildScene(IBrush? brush) private IImmutableBrush? ConvertBrush(IBrush? brush)
{ {
if (brush is VisualBrush visualBrush) if (brush is IMutableBrush mutable)
{ return mutable.ToImmutable();
var visual = visualBrush.Visual; if (brush is ISceneBrush sceneBrush)
return sceneBrush.CreateContent();
if (visual != null) return (IImmutableBrush?)brush;
{
// TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer
// We should directly reference the corresponding CompositionVisual (which should
// be attached to the same composition target) like UWP does.
// Render-able visuals shouldn't be dangling unattached
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
ImmediateRenderer.Render(visual, new DrawingContext(recorder));
var drawList = recorder.EndUpdate();
drawList.Size = visual.Bounds.Size;
return drawList;
}
}
return null;
} }
} }

30
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server;
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{ {
private IDrawingContextImpl _impl; private IDrawingContextImpl _impl;
private readonly VisualBrushRenderer _visualBrushRenderer;
public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) public CompositorDrawingContextProxy(IDrawingContextImpl impl)
{ {
_impl = impl; _impl = impl;
_visualBrushRenderer = visualBrushRenderer;
}
// This is a hack to make it work with the current way of handling visual brushes
public CompositionDrawList? VisualBrushDrawList
{
get => _visualBrushRenderer.VisualBrushDrawList;
set => _visualBrushRenderer.VisualBrushDrawList = value;
} }
public Matrix PostTransform { get; set; } = Matrix.Identity; public Matrix PostTransform { get; set; } = Matrix.Identity;
@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
} }
public object? GetFeature(Type t) => _impl.GetFeature(t); public object? GetFeature(Type t) => _impl.GetFeature(t);
public class VisualBrushRenderer : IVisualBrushRenderer
{
public CompositionDrawList? VisualBrushDrawList { get; set; }
public Size GetRenderTargetSize(IVisualBrush brush)
{
return VisualBrushDrawList?.Size ?? default;
}
public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
if (VisualBrushDrawList != null)
{
foreach (var cmd in VisualBrushDrawList)
cmd.Item.Render(context);
}
}
}
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{ {

7
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server
Readback.CompleteWrite(Revision); Readback.CompleteWrite(Revision);
_redrawRequested = false; _redrawRequested = false;
using (var targetContext = _renderTarget.CreateDrawingContext(null)) using (var targetContext = _renderTarget.CreateDrawingContext())
{ {
var layerSize = Size * Scaling; var layerSize = Size * Scaling;
if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server
if (!_dirtyRect.IsDefault) if (!_dirtyRect.IsDefault)
{ {
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); using (var context = _layer.CreateDrawingContext())
using (var context = _layer.CreateDrawingContext(visualBrushHelper))
{ {
context.PushClip(_dirtyRect); context.PushClip(_dirtyRect);
context.Clear(Colors.Transparent); context.Clear(Colors.Transparent);
Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect);
context.PopClip(); context.PopClip();
} }
} }

29
src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs

@ -1,29 +0,0 @@
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines a renderer used to render a visual brush to a bitmap.
/// </summary>
[Unstable]
public interface IVisualBrushRenderer
{
/// <summary>
/// Gets the size of the intermediate render target to which the visual brush should be
/// drawn.
/// </summary>
/// <param name="brush">The visual brush.</param>
/// <returns>The size of the intermediate render target to create.</returns>
Size GetRenderTargetSize(IVisualBrush brush);
/// <summary>
/// Renders a visual brush to a bitmap.
/// </summary>
/// <param name="context">The drawing context to render to.</param>
/// <param name="brush">The visual brush.</param>
/// <returns>A bitmap containing the rendered brush.</returns>
void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush);
}
}

37
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -14,19 +14,8 @@ namespace Avalonia.Rendering
/// a simple tree traversal. /// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// </summary> /// </summary>
internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer internal class ImmediateRenderer
{ {
/// <summary>
/// Renders a visual to a render target.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="target">The render target.</param>
public static void Render(Visual visual, IRenderTarget target)
{
using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
Render(context, visual, visual.Bounds);
}
/// <summary> /// <summary>
/// Renders a visual to a drawing context. /// Renders a visual to a drawing context.
/// </summary> /// </summary>
@ -36,28 +25,6 @@ namespace Avalonia.Rendering
{ {
Render(context, visual, visual.Bounds); Render(context, visual, visual.Bounds);
} }
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
(brush.Visual as IVisualBrushInitialize)?.EnsureInitialized();
return brush.Visual?.Bounds.Size ?? default;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
if (brush.Visual is { } visual)
{
Render(new DrawingContext(context), visual, visual.Bounds);
}
}
internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
{
Render(context, visual, visual.Bounds);
}
private static Rect GetTransformedBounds(Visual visual) private static Rect GetTransformedBounds(Visual visual)
{ {
@ -75,7 +42,7 @@ namespace Avalonia.Rendering
} }
private static void Render(DrawingContext context, Visual visual, Rect clipRect) public static void Render(DrawingContext context, Visual visual, Rect clipRect)
{ {
var opacity = visual.Opacity; var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds; var clipToBounds = visual.ClipToBounds;

15
src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs

@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// Base class for draw operations that can use a brush. /// Base class for draw operations that can use a brush.
/// </summary> /// </summary>
internal abstract class BrushDrawOperation : DrawOperation internal abstract class BrushDrawOperation : DrawOperationWithTransform
{ {
public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) public IImmutableBrush? Brush { get; }
public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush)
: base(bounds, transform) : base(bounds, transform)
{ {
Aux = aux; Brush = brush;
} }
/// <summary>
/// Auxiliary data required to draw the brush
/// </summary>
public IDisposable? Aux { get; }
public override void Dispose() public override void Dispose()
{ {
Aux?.Dispose(); (Brush as ISceneBrushContent)?.Dispose();
base.Dispose(); base.Dispose();
} }
} }

4
src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs

@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// A node in the scene graph which represents a clip push or pop. /// A node in the scene graph which represents a clip push or pop.
/// </summary> /// </summary>
internal class ClipNode : IDrawOperation internal class ClipNode : IDrawOperationWithTransform
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public void Render(IDrawingContextImpl context) public void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
if (Clip.HasValue) if (Clip.HasValue)
{ {
context.PushClip(Clip.Value); context.PushClip(Clip.Value);

39
src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs

@ -4,30 +4,19 @@ using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph namespace Avalonia.Rendering.SceneGraph
{ {
internal sealed class CustomDrawOperation : DrawOperation internal sealed class CustomDrawOperation : DrawOperationWithTransform
{ {
public Matrix Transform { get; }
public ICustomDrawOperation Custom { get; } public ICustomDrawOperation Custom { get; }
public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform)
: base(custom.Bounds, transform) : base(custom.Bounds, transform)
{ {
Transform = transform;
Custom = custom; Custom = custom;
} }
public override bool HitTest(Point p) public override bool HitTest(Point p) => Custom.HitTest(p);
{
if (Transform.HasInverse)
{
return Custom.HitTest(p * Transform.Invert());
}
return false;
}
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
Custom.Render(context); Custom.Render(context);
} }
@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph
Transform == transform && Custom?.Equals(custom) == true; Transform == transform && Custom?.Equals(custom) == true;
} }
public interface ICustomDrawOperation : IDrawOperation, IEquatable<ICustomDrawOperation> public interface ICustomDrawOperation : IEquatable<ICustomDrawOperation>, IDisposable
{ {
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
} }
} }

10
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph
{ {
} }
} }
internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform
{
protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform)
{
Transform = transform;
}
public Matrix Transform { get; }
}
} }

37
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph
{ {
public EllipseNode( public EllipseNode(
Matrix transform, Matrix transform,
IBrush? brush, IImmutableBrush? brush,
IPen? pen, IPen? pen,
Rect rect, Rect rect)
IDisposable? aux = null) : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush)
: base(rect.Inflate(pen?.Thickness ?? 0), transform, aux)
{ {
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable(); Pen = pen?.ToImmutable();
Rect = rect; Rect = rect;
} }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary> /// <summary>
/// Gets the stroke pen. /// Gets the stroke pen.
/// </summary> /// </summary>
public ImmutablePen? Pen { get; } public ImmutablePen? Pen { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary> /// <summary>
/// Gets the rect of the ellipse to draw. /// Gets the rect of the ellipse to draw.
/// </summary> /// </summary>
@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph
rect.Equals(Rect); rect.Equals(Rect);
} }
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect);
{
context.Transform = Transform;
context.DrawEllipse(Brush, Pen, Rect);
}
public override bool HitTest(Point p) public override bool HitTest(Point p)
{ {
if (!Transform.TryInvert(out Matrix inverted))
{
return false;
}
p *= inverted;
var center = Rect.Center; var center = Rect.Center;
var strokeThickness = Pen?.Thickness ?? 0; var strokeThickness = Pen?.Thickness ?? 0;
@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph
return false; return false;
} }
public override void Dispose()
{
(Brush as ISceneBrushContent)?.Dispose();
}
} }
} }

24
src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// A node in the scene graph which represents a rectangle draw. /// A node in the scene graph which represents a rectangle draw.
/// </summary> /// </summary>
internal class ExperimentalAcrylicNode : DrawOperation internal class ExperimentalAcrylicNode : DrawOperationWithTransform
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RectangleNode"/> class. /// Initializes a new instance of the <see cref="RectangleNode"/> class.
@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph
RoundedRect rect) RoundedRect rect)
: base(rect.Rect, transform) : base(rect.Rect, transform)
{ {
Transform = transform;
Material = material.ToImmutable(); Material = material.ToImmutable();
Rect = rect; Rect = rect;
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
public IExperimentalAcrylicMaterial Material { get; } public IExperimentalAcrylicMaterial Material { get; }
/// <summary> /// <summary>
@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
if(context is IDrawingContextWithAcrylicLikeSupport idc) if(context is IDrawingContextWithAcrylicLikeSupport idc)
{ {
idc.DrawRectangle(Material, Rect); idc.DrawRectangle(Material, Rect);
@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool HitTest(Point p) public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p);
{
// TODO: This doesn't respect CornerRadius yet.
if (Transform.HasInverse)
{
p *= Transform.Invert();
var rect = Rect.Rect;
return rect.ContainsExclusive(p);
}
return false;
}
} }
} }

4
src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs

@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// A node in the scene graph which represents a geometry clip push or pop. /// A node in the scene graph which represents a geometry clip push or pop.
/// </summary> /// </summary>
internal class GeometryClipNode : IDrawOperation internal class GeometryClipNode : IDrawOperationWithTransform
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a /// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public void Render(IDrawingContextImpl context) public void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
if (Clip != null) if (Clip != null)
{ {
context.PushGeometryClip(Clip); context.PushGeometryClip(Clip);

30
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="geometry">The geometry.</param> /// <param name="geometry">The geometry.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param> /// <param name="aux">Auxiliary data required to draw the brush.</param>
public GeometryNode(Matrix transform, public GeometryNode(Matrix transform,
IBrush? brush, IImmutableBrush? brush,
IPen? pen, IPen? pen,
IGeometryImpl geometry, IGeometryImpl geometry)
IDisposable? aux) : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush)
: base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux)
{ {
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable(); Pen = pen?.ToImmutable();
Geometry = geometry; Geometry = geometry;
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary> /// <summary>
/// Gets the stroke pen. /// Gets the stroke pen.
/// </summary> /// </summary>
@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry); context.DrawGeometry(Brush, Pen, Geometry);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool HitTest(Point p) public override bool HitTest(Point p)
{ {
if (Transform.HasInverse) return (Brush != null && Geometry.FillContains(p)) ||
{ (Pen != null && Geometry.StrokeContains(Pen, p));
p *= Transform.Invert();
return (Brush != null && Geometry.FillContains(p)) ||
(Pen != null && Geometry.StrokeContains(Pen, p));
}
return false;
} }
} }
} }

33
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="aux">Auxiliary data required to draw the brush.</param> /// <param name="aux">Auxiliary data required to draw the brush.</param>
public GlyphRunNode( public GlyphRunNode(
Matrix transform, Matrix transform,
IBrush foreground, IImmutableBrush foreground,
IRef<IGlyphRunImpl> glyphRun, IRef<IGlyphRunImpl> glyphRun)
IDisposable? aux = null) : base(new Rect(glyphRun.Item.Size), transform, foreground)
: base(new Rect(glyphRun.Item.Size), transform, aux)
{ {
Transform = transform;
Foreground = foreground.ToImmutable();
GlyphRun = glyphRun.Clone(); GlyphRun = glyphRun.Clone();
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary> /// <summary>
/// Gets the glyph run to draw. /// Gets the glyph run to draw.
/// </summary> /// </summary>
public IRef<IGlyphRunImpl> GlyphRun { get; } public IRef<IGlyphRunImpl> GlyphRun { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun);
{
context.Transform = Transform;
context.DrawGlyphRun(Foreground, GlyphRun);
}
/// <summary> /// <summary>
/// Determines if this draw operation equals another. /// Determines if this draw operation equals another.
@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph
internal bool Equals(Matrix transform, IBrush foreground, IRef<IGlyphRunImpl> glyphRun) internal bool Equals(Matrix transform, IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
{ {
return transform == Transform && return transform == Transform &&
Equals(foreground, Foreground) && Equals(foreground, Brush) &&
Equals(glyphRun.Item, GlyphRun.Item); Equals(glyphRun.Item, GlyphRun.Item);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p);
public override void Dispose() public override void Dispose()
{ {
GlyphRun?.Dispose(); GlyphRun?.Dispose();
base.Dispose();
} }
} }
} }

10
src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs

@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// Represents a node in the low-level scene graph that represents geometry. /// Represents a node in the low-level scene graph that represents geometry.
/// </summary> /// </summary>
public interface IDrawOperation : IDisposable internal interface IDrawOperation : IDisposable
{ {
/// <summary> /// <summary>
/// Gets the bounds of the visible content in the node in global coordinates. /// Gets the bounds of the visible content in the node in global coordinates.
@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context); void Render(IDrawingContextImpl context);
} }
internal interface IDrawOperationWithTransform : IDrawOperation
{
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
Matrix Transform { get; }
}
} }

21
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// A node in the scene graph which represents an image draw. /// A node in the scene graph which represents an image draw.
/// </summary> /// </summary>
internal class ImageNode : DrawOperation internal class ImageNode : DrawOperationWithTransform
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageNode"/> class. /// Initializes a new instance of the <see cref="ImageNode"/> class.
@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform) : base(destRect, transform)
{ {
Transform = transform;
Source = source.Clone(); Source = source.Clone();
Opacity = opacity; Opacity = opacity;
SourceRect = sourceRect; SourceRect = sourceRect;
DestRect = destRect; DestRect = destRect;
BitmapInterpolationMode = bitmapInterpolationMode; BitmapInterpolationMode = bitmapInterpolationMode;
SourceVersion = Source.Item.Version; SourceVersion = Source.Item.Version;
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary> /// <summary>
/// Gets the image to draw. /// Gets the image to draw.
@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </value> /// </value>
public BitmapInterpolationMode BitmapInterpolationMode { get; } public BitmapInterpolationMode BitmapInterpolationMode { get; }
/// <summary>
/// The bitmap blending mode.
/// </summary>
/// <value>
/// The blending mode.
/// </value>
public BitmapBlendingMode BitmapBlendingMode { get; }
/// <summary> /// <summary>
/// Determines if this draw operation equals another. /// Determines if this draw operation equals another.
/// </summary> /// </summary>
@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
public override void Dispose() public override void Dispose()
{ {

19
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary> /// <summary>
/// A node in the scene graph which represents a line draw. /// A node in the scene graph which represents a line draw.
/// </summary> /// </summary>
internal class LineNode : BrushDrawOperation internal class LineNode : DrawOperationWithTransform
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GeometryNode"/> class. /// Initializes a new instance of the <see cref="GeometryNode"/> class.
@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph
Matrix transform, Matrix transform,
IPen pen, IPen pen,
Point p1, Point p1,
Point p2, Point p2)
IDisposable? aux = null) : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux)
{ {
Transform = transform;
Pen = pen.ToImmutable(); Pen = pen.ToImmutable();
P1 = p1; P1 = p1;
P2 = p2; P2 = p2;
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary> /// <summary>
/// Gets the stroke pen. /// Gets the stroke pen.
/// </summary> /// </summary>
@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
context.Transform = Transform;
context.DrawLine(Pen, P1, P2); context.DrawLine(Pen, P1, P2);
} }
public override bool HitTest(Point p) public override bool HitTest(Point p)
{ {
if (!Transform.HasInverse)
return false;
p *= Transform.Invert();
var halfThickness = Pen.Thickness / 2; var halfThickness = Pen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness; var minX = Math.Min(P1.X, P2.X) - halfThickness;
var maxX = Math.Max(P1.X, P2.X) + halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness;

41
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="mask">The opacity mask to push.</param> /// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param> /// <param name="bounds">The bounds of the mask.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param> /// <param name="aux">Auxiliary data required to draw the brush.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
: base(default, Matrix.Identity, aux) : base(default, Matrix.Identity, mask)
{ {
Mask = mask.ToImmutable();
MaskBounds = bounds; MaskBounds = bounds;
} }
/// <summary>
/// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
/// opacity mask pop.
/// </summary>
public OpacityMaskNode()
: base(default, Matrix.Identity, null)
{
}
/// <summary>
/// Gets the mask to be pushed or null if the operation represents a pop.
/// </summary>
public IBrush? Mask { get; }
/// <summary> /// <summary>
/// Gets the bounds of the opacity mask or null if the operation represents a pop. /// Gets the bounds of the opacity mask or null if the operation represents a pop.
/// </summary> /// </summary>
@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent /// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object. /// allocation of a not-yet-constructed draw operation object.
/// </remarks> /// </remarks>
public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds;
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context)
{ {
if (Mask != null) context.PushOpacityMask(Brush!, MaskBounds!.Value);
{
context.PushOpacityMask(Mask, MaskBounds!.Value);
}
else
{
context.PopOpacityMask();
}
} }
} }
internal class OpacityMaskPopNode : DrawOperation
{
public OpacityMaskPopNode() : base(default, Matrix.Identity)
{
}
public override bool HitTest(Point p) => false;
public override void Render(IDrawingContextImpl context) => context.PopOpacityMask();
}
} }

52
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="aux">Auxiliary data required to draw the brush.</param> /// <param name="aux">Auxiliary data required to draw the brush.</param>
public RectangleNode( public RectangleNode(
Matrix transform, Matrix transform,
IBrush? brush, IImmutableBrush? brush,
IPen? pen, IPen? pen,
RoundedRect rect, RoundedRect rect,
BoxShadows boxShadows, BoxShadows boxShadows)
IDisposable? aux = null) : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux)
{ {
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable(); Pen = pen?.ToImmutable();
Rect = rect; Rect = rect;
BoxShadows = boxShadows; BoxShadows = boxShadows;
} }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary> /// <summary>
/// Gets the stroke pen. /// Gets the stroke pen.
/// </summary> /// </summary>
@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(IDrawingContextImpl context) public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
{
context.Transform = Transform;
context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
}
/// <inheritdoc/> /// <inheritdoc/>
public override bool HitTest(Point p) public override bool HitTest(Point p)
{ {
// TODO: This doesn't respect CornerRadius yet. if (Brush != null)
if (Transform.HasInverse)
{ {
p *= Transform.Invert(); var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
return rect.ContainsExclusive(p);
if (Brush != null) }
{ else
var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); {
return rect.ContainsExclusive(p); var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
} var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
else return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
{
var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
}
} }
return false;
} }
} }
} }

7
src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs

@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace Avalonia.Threading namespace Avalonia.Threading
{ {
public class ThreadSafeObjectPool<T> where T : class, new() internal class ThreadSafeObjectPool<T> where T : class, new()
{ {
private Stack<T> _stack = new Stack<T>(); private Stack<T> _stack = new Stack<T>();
public static ThreadSafeObjectPool<T> Default { get; } = new ThreadSafeObjectPool<T>(); public static ThreadSafeObjectPool<T> Default { get; } = new ThreadSafeObjectPool<T>();
@ -17,11 +17,14 @@ namespace Avalonia.Threading
} }
} }
public void Return(T obj) public void ReturnAndSetNull(ref T? obj)
{ {
if (obj == null)
return;
lock (_stack) lock (_stack)
{ {
_stack.Push(obj); _stack.Push(obj);
obj = null;
} }
} }
} }

2
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

@ -82,7 +82,7 @@ namespace Avalonia.Controls
public sealed override void Render(DrawingContext context) public sealed override void Render(DrawingContext context)
{ {
if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) if (context is IDrawingContextWithAcrylicLikeSupport idc)
{ {
var cornerRadius = CornerRadius; var cornerRadius = CornerRadius;

2
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -148,7 +148,7 @@ namespace Avalonia.Controls.Utils
var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft); _cornerRadius.BottomRight, _cornerRadius.BottomLeft);
context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); context.DrawRectangle(background, pen, rrect, boxShadows);
} }
} }

4
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -325,7 +325,7 @@ namespace Avalonia.Headless
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
return new HeadlessDrawingContextStub(); return new HeadlessDrawingContextStub();
} }
@ -491,7 +491,7 @@ namespace Avalonia.Headless
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
return new HeadlessDrawingContextStub(); return new HeadlessDrawingContextStub();
} }

2
src/Avalonia.X11/X11CursorFactory.cs

@ -115,7 +115,7 @@ namespace Avalonia.X11
using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) using (var cpuContext = platformRenderInterface.CreateBackendContext(null))
using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this }))
using (var ctx = renderTarget.CreateDrawingContext(null)) using (var ctx = renderTarget.CreateDrawingContext())
{ {
var r = new Rect(_pixelSize.ToSize(1)); var r = new Rect(_pixelSize.ToSize(1));
ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r);

2
src/Avalonia.X11/X11IconLoader.cs

@ -43,7 +43,7 @@ namespace Avalonia.X11
_bdata = new uint[_width * _height]; _bdata = new uint[_width * _height];
using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null)) using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null))
using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using(var rt = cpuContext.CreateRenderTarget(new[]{this}))
using (var ctx = rt.CreateDrawingContext(null)) using (var ctx = rt.CreateDrawingContext())
ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size),
new Rect(0, 0, _width, _height)); new Rect(0, 0, _width, _height));
Data = new UIntPtr[_width * _height + 2]; Data = new UIntPtr[_width * _height + 2];

136
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.Rendering.Utilities; using Avalonia.Rendering.Utilities;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Skia.Helpers;
using SkiaSharp; using SkiaSharp;
using ISceneBrush = Avalonia.Media.ISceneBrush;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
@ -25,7 +27,6 @@ namespace Avalonia.Skia
private readonly Stack<double> _opacityStack = new(); private readonly Stack<double> _opacityStack = new();
private readonly Stack<BitmapBlendingMode> _blendingModeStack = new(); private readonly Stack<BitmapBlendingMode> _blendingModeStack = new();
private readonly Matrix? _postTransform; private readonly Matrix? _postTransform;
private readonly IVisualBrushRenderer? _visualBrushRenderer;
private double _currentOpacity = 1.0f; private double _currentOpacity = 1.0f;
private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver;
private readonly bool _canTextUseLcdRendering; private readonly bool _canTextUseLcdRendering;
@ -61,12 +62,7 @@ namespace Avalonia.Skia
/// Dpi of drawings. /// Dpi of drawings.
/// </summary> /// </summary>
public Vector Dpi; public Vector Dpi;
/// <summary>
/// Visual brush renderer.
/// </summary>
public IVisualBrushRenderer? VisualBrushRenderer;
/// <summary> /// <summary>
/// Render text without Lcd rendering. /// Render text without Lcd rendering.
/// </summary> /// </summary>
@ -141,7 +137,6 @@ namespace Avalonia.Skia
?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
_dpi = createInfo.Dpi; _dpi = createInfo.Dpi;
_visualBrushRenderer = createInfo.VisualBrushRenderer;
_disposables = disposables; _disposables = disposables;
_canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
_grContext = createInfo.GrContext; _grContext = createInfo.GrContext;
@ -908,7 +903,7 @@ namespace Avalonia.Skia
paintWrapper.AddDisposable(intermediate); paintWrapper.AddDisposable(intermediate);
using (var context = intermediate.CreateDrawingContext(null)) using (var context = intermediate.CreateDrawingContext())
{ {
var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96));
var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi));
@ -970,36 +965,98 @@ namespace Avalonia.Skia
} }
} }
/// <summary> private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content,
/// Configure paint wrapper to use visual brush. Size targetSize)
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="visualBrush">Visual brush.</param>
/// <param name="visualBrushRenderer">Visual brush renderer.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush,
IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage)
{ {
if (visualBrushRenderer == null) if(content.UseScalableRasterization)
{ ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize);
throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); else
} ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize);
}
var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
{
var rect = content.Rect;
var intermediateSize = rect.Size;
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
{ {
var intermediate = CreateRenderTarget(intermediateSize, false); using var intermediate = CreateRenderTarget(intermediateSize, false);
using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) using (var ctx = intermediate.CreateDrawingContext())
{ {
ctx.Clear(Colors.Transparent); ctx.Clear(Colors.Transparent);
content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
} }
tileBrushImage = intermediate; ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate);
paintWrapper.AddDisposable(tileBrushImage); }
}
private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
{
var rect = content.Rect;
var contentSize = rect.Size;
if (contentSize.Width <= 0 || contentSize.Height <= 0)
{
paintWrapper.Paint.Color = SKColor.Empty;
return;
}
var tileBrush = content.Brush;
var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y);
var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize);
transform *= calc.IntermediateTransform;
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi);
using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize))
{
ctx.PushClip(calc.IntermediateClip);
content.Render(ctx, transform);
ctx.PopClip();
}
using var picture = pictureTarget.GetPicture();
var paintTransform =
tileBrush.TileMode != TileMode.None
? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
: SKMatrix.CreateIdentity();
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
SKShaderTileMode tileY =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
paintTransform = SKMatrix.Concat(paintTransform,
SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
if (tileBrush.Transform is { })
{
var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(origin);
var brushTransform = (-offset) * tileBrush.Transform.Value * (offset);
paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix());
}
using (var shader = picture.ToShader(tileX, tileY, paintTransform,
new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height)))
{
paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
paintWrapper.Paint.Shader = shader;
} }
} }
@ -1113,12 +1170,25 @@ namespace Avalonia.Skia
} }
var tileBrush = brush as ITileBrush; var tileBrush = brush as ITileBrush;
var visualBrush = brush as IVisualBrush;
var tileBrushImage = default(IDrawableBitmapImpl); var tileBrushImage = default(IDrawableBitmapImpl);
if (visualBrush != null) if (brush is ISceneBrush sceneBrush)
{ {
ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); using (var content = sceneBrush.CreateContent())
{
if (content != null)
{
ConfigureSceneBrushContent(ref paintWrapper, content, targetSize);
return paintWrapper;
}
else
paint.Color = default;
}
}
else if (brush is ISceneBrushContent sceneBrushContent)
{
ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize);
return paintWrapper;
} }
else else
{ {

3
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@ -36,7 +36,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var framebuffer = _platformSurface.Lock(); var framebuffer = _platformSurface.Lock();
var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height,
@ -55,7 +55,6 @@ namespace Avalonia.Skia
{ {
Surface = _framebufferSurface, Surface = _framebufferSurface,
Dpi = framebuffer.Dpi, Dpi = framebuffer.Dpi,
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = true DisableTextLcdRendering = true
}; };

3
src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@ -22,7 +22,7 @@ namespace Avalonia.Skia
_renderTarget.Dispose(); _renderTarget.Dispose();
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var session = _renderTarget.BeginRenderingSession(); var session = _renderTarget.BeginRenderingSession();
@ -31,7 +31,6 @@ namespace Avalonia.Skia
GrContext = session.GrContext, GrContext = session.GrContext,
Surface = session.SkSurface, Surface = session.SkSurface,
Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor,
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = true, DisableTextLcdRendering = true,
Gpu = _skiaGpu, Gpu = _skiaGpu,
CurrentSession = session CurrentSession = session

3
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@ -15,13 +15,12 @@ namespace Avalonia.Skia.Helpers
/// <param name="dpi"></param> /// <param name="dpi"></param>
/// <param name="visualBrushRenderer"></param> /// <param name="visualBrushRenderer"></param>
/// <returns>DrawingContext</returns> /// <returns>DrawingContext</returns>
public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null) public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi)
{ {
var createInfo = new DrawingContextImpl.CreateInfo var createInfo = new DrawingContextImpl.CreateInfo
{ {
Canvas = canvas, Canvas = canvas,
Dpi = dpi, Dpi = dpi,
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = true, DisableTextLcdRendering = true,
}; };

10
src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs

@ -60,5 +60,15 @@ namespace Avalonia.Skia.Helpers
} }
} }
} }
// This method is here mostly for debugging purposes
internal static void SavePicture(SKPicture picture, float scale, string path)
{
var snapshotSize = new SKSizeI((int)Math.Ceiling(picture.CullRect.Width * scale),
(int)Math.Ceiling(picture.CullRect.Height * scale));
using var snap =
SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale));
SaveImage(snap, path);
}
} }
} }

55
src/Skia/Avalonia.Skia/PictureRenderTarget.cs

@ -0,0 +1,55 @@
using System;
using Avalonia.Platform;
using Avalonia.Reactive;
using SkiaSharp;
namespace Avalonia.Skia;
internal class PictureRenderTarget : IDisposable
{
private readonly ISkiaGpu? _gpu;
private readonly GRContext? _grContext;
private readonly Vector _dpi;
private SKPicture? _picture;
public PictureRenderTarget(ISkiaGpu? gpu, GRContext? grContext, Vector dpi)
{
_gpu = gpu;
_grContext = grContext;
_dpi = dpi;
}
public SKPicture GetPicture()
{
var rv = _picture ?? throw new InvalidOperationException();
_picture = null;
return rv;
}
public IDrawingContextImpl CreateDrawingContext(Size size)
{
var recorder = new SKPictureRecorder();
var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96),
(float)(size.Height * _dpi.Y / 96)));
canvas.RestoreToCount(-1);
canvas.ResetMatrix();
var createInfo = new DrawingContextImpl.CreateInfo
{
Canvas = canvas,
Dpi = _dpi,
DisableTextLcdRendering = true,
GrContext = _grContext,
Gpu = _gpu,
};
return new DrawingContextImpl(createInfo, Disposable.Create(() =>
{
_picture = recorder.EndRecording();
canvas.Dispose();
recorder.Dispose();
}));
}
public void Dispose() => _picture?.Dispose();
}

3
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@ -97,7 +97,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
_canvas.RestoreToCount(-1); _canvas.RestoreToCount(-1);
_canvas.ResetMatrix(); _canvas.ResetMatrix();
@ -106,7 +106,6 @@ namespace Avalonia.Skia
{ {
Surface = _surface.Surface, Surface = _surface.Surface,
Dpi = Dpi, Dpi = Dpi,
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = _disableLcdRendering, DisableTextLcdRendering = _disableLcdRendering,
GrContext = _grContext, GrContext = _grContext,
Gpu = _gpu, Gpu = _gpu,

4
src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs

@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1
_externalRenderTargetProvider.DestroyRenderTarget(); _externalRenderTargetProvider.DestroyRenderTarget();
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); var target = _externalRenderTargetProvider.GetOrCreateRenderTarget();
_externalRenderTargetProvider.BeforeDrawing(); _externalRenderTargetProvider.BeforeDrawing();
return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => return new DrawingContextImpl( null, target, null, () =>
{ {
try try
{ {

8
src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs

@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1
{ {
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var locked = _surface.Lock(); var locked = _surface.Lock();
if (locked.Format == PixelFormat.Rgb565) if (locked.Format == PixelFormat.Rgb565)
@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
} }
return new FramebufferShim(locked) return new FramebufferShim(locked)
.CreateDrawingContext(visualBrushRenderer); .CreateDrawingContext();
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;
@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1
_target = target; _target = target;
} }
public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public override IDrawingContextImpl CreateDrawingContext()
{ {
return base.CreateDrawingContext(visualBrushRenderer, () => return base.CreateDrawingContext(() =>
{ {
using (var l = WicImpl.Lock(BitmapLockFlags.Read)) using (var l = WicImpl.Lock(BitmapLockFlags.Read))
{ {

32
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
internal class DrawingContextImpl : IDrawingContextImpl internal class DrawingContextImpl : IDrawingContextImpl
{ {
private readonly IVisualBrushRenderer _visualBrushRenderer;
private readonly ILayerFactory _layerFactory; private readonly ILayerFactory _layerFactory;
private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
private readonly DeviceContext _deviceContext; private readonly DeviceContext _deviceContext;
@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media
/// <param name="swapChain">An optional swap chain associated with this drawing context.</param> /// <param name="swapChain">An optional swap chain associated with this drawing context.</param>
/// <param name="finishedCallback">An optional delegate to be called when context is disposed.</param> /// <param name="finishedCallback">An optional delegate to be called when context is disposed.</param>
public DrawingContextImpl( public DrawingContextImpl(
IVisualBrushRenderer visualBrushRenderer,
ILayerFactory layerFactory, ILayerFactory layerFactory,
SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.Direct2D1.RenderTarget renderTarget,
SharpDX.DXGI.SwapChain1 swapChain = null, SharpDX.DXGI.SwapChain1 swapChain = null,
Action finishedCallback = null) Action finishedCallback = null)
{ {
_visualBrushRenderer = visualBrushRenderer;
_layerFactory = layerFactory; _layerFactory = layerFactory;
_renderTarget = renderTarget; _renderTarget = renderTarget;
_swapChain = swapChain; _swapChain = swapChain;
@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media
var radialGradientBrush = brush as IRadialGradientBrush; var radialGradientBrush = brush as IRadialGradientBrush;
var conicGradientBrush = brush as IConicGradientBrush; var conicGradientBrush = brush as IConicGradientBrush;
var imageBrush = brush as IImageBrush; var imageBrush = brush as IImageBrush;
var visualBrush = brush as IVisualBrush; var sceneBrush = brush as ISceneBrush;
var sceneBrushContent = brush as ISceneBrushContent;
if (solidColorBrush != null) if (solidColorBrush != null)
{ {
@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media
(BitmapImpl)imageBrush.Source.PlatformImpl.Item, (BitmapImpl)imageBrush.Source.PlatformImpl.Item,
destinationSize); destinationSize);
} }
else if (visualBrush != null) else if (sceneBrush != null || sceneBrushContent != null)
{ {
if (_visualBrushRenderer != null) sceneBrushContent ??= sceneBrush.CreateContent();
if (sceneBrushContent != null)
{ {
var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); var rect = sceneBrushContent.Rect;
var intermediateSize = rect.Size;
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
{ {
@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media
var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi);
using (var intermediate = new BitmapRenderTarget( using (var intermediate = new BitmapRenderTarget(
_deviceContext, _deviceContext,
CompatibleRenderTargetOptions.None, CompatibleRenderTargetOptions.None,
pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) pixelSize.ToSizeWithDpi(dpi).ToSharpDX()))
{ {
using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
{ {
intermediate.Clear(null); intermediate.Clear(null);
_visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); sceneBrushContent.Render(ctx,
rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
} }
return new ImageBrushImpl( return new ImageBrushImpl(
visualBrush, sceneBrushContent.Brush,
_deviceContext, _deviceContext,
new D2DBitmapImpl(intermediate.Bitmap), new D2DBitmapImpl(intermediate.Bitmap),
destinationSize); destinationSize);
} }
} }
} }
else
{
throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
}
} }
return new SolidColorBrushImpl(null, _deviceContext); return new SolidColorBrushImpl(null, _deviceContext);

2
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media
CompatibleRenderTargetOptions.None, CompatibleRenderTargetOptions.None,
calc.IntermediateSize.ToSharpDX()); calc.IntermediateSize.ToSharpDX());
using (var context = new RenderTarget(result).CreateDrawingContext(null)) using (var context = new RenderTarget(result).CreateDrawingContext())
{ {
var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height);
var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi));

4
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging
return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); return new D2DRenderTargetBitmapImpl(bitmapRenderTarget);
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); return new DrawingContextImpl( this, _renderTarget, null, () => Version++);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;

8
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs

@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media
base.Dispose(); base.Dispose();
} }
public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public virtual IDrawingContextImpl CreateDrawingContext()
=> CreateDrawingContext(visualBrushRenderer, null); => CreateDrawingContext(null);
public bool IsCorrupted => false; public bool IsCorrupted => false;
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) public IDrawingContextImpl CreateDrawingContext(Action finishedCallback)
{ {
return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => return new DrawingContextImpl(null, _renderTarget, finishedCallback: () =>
{ {
Version++; Version++;
finishedCallback?.Invoke(); finishedCallback?.Invoke();

4
src/Windows/Avalonia.Direct2D1/RenderTarget.cs

@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1
/// Creates a drawing context for a rendering session. /// Creates a drawing context for a rendering session.
/// </summary> /// </summary>
/// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns> /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); return new DrawingContextImpl(this, _renderTarget);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;

4
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1
/// Creates a drawing context for a rendering session. /// Creates a drawing context for a rendering session.
/// </summary> /// </summary>
/// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns> /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var size = GetWindowSize(); var size = GetWindowSize();
var dpi = GetWindowDpi(); var dpi = GetWindowDpi();
@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
Resize(); Resize();
} }
return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); return new DrawingContextImpl(this, _deviceContext, _swapChain);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;

2
tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs

@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests
private DrawingContext CreateDrawingContext() private DrawingContext CreateDrawingContext()
{ {
return new DrawingContext(Mock.Of<IDrawingContextImpl>()); return new PlatformDrawingContext(Mock.Of<IDrawingContextImpl>());
} }
private class TestControl : Control private class TestControl : Control

4
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
new Matrix(), new Matrix(),
Brushes.Black, Brushes.Black,
null, null,
geometry, default); geometry);
geometryNode.HitTest(new Point()); geometryNode.HitTest(new Point());
} }
@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
private class TestRectangleDrawOperation : RectangleNode private class TestRectangleDrawOperation : RectangleNode
{ {
public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen)
: base(transform, pen.Brush, pen, bounds, new BoxShadows()) : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows())
{ {
} }

4
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs

@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
[InlineData(0, 101, false)] [InlineData(0, 101, false)]
public void FillOnly_HitTest(double x, double y, bool inside) public void FillOnly_HitTest(double x, double y, bool inside)
{ {
var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null); var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100));
var point = new Point(x, y); var point = new Point(x, y);
@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
[InlineData(0, 101, false)] [InlineData(0, 101, false)]
public void StrokeOnly_HitTest(double x, double y, bool inside) public void StrokeOnly_HitTest(double x, double y, bool inside)
{ {
var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null); var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100));
var point = new Point(x, y); var point = new Point(x, y);

2
tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs

@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering
_lineFill = new Line { Fill = new SolidColorBrush() }; _lineFill = new Line { Fill = new SolidColorBrush() };
_lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() };
_drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true);
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new NullRenderingPlatform()); AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new NullRenderingPlatform());
} }

4
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@ -76,7 +76,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var r = Avalonia.AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var r = Avalonia.AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
using(var cpuContext = r.CreateBackendContext(null)) using(var cpuContext = r.CreateBackendContext(null))
using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) using (var target = cpuContext.CreateRenderTarget(new object[] { fb }))
using (var ctx = target.CreateDrawingContext(null)) using (var ctx = target.CreateDrawingContext())
{ {
ctx.Clear(Colors.Transparent); ctx.Clear(Colors.Transparent);
ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80));
@ -90,7 +90,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
fb.Deallocate(); fb.Deallocate();
using (var rtb = new RenderTargetBitmap(new PixelSize(100, 100), new Vector(96, 96))) using (var rtb = new RenderTargetBitmap(new PixelSize(100, 100), new Vector(96, 96)))
{ {
using (var ctx = rtb.CreateDrawingContext(null)) using (var ctx = rtb.CreateDrawingContext())
{ {
ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100)); ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100));
ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10)); ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10));

94
tests/Avalonia.RenderTests/Media/TileBrushTests.cs

@ -0,0 +1,94 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#else
namespace Avalonia.Direct2D1.RenderTests.Media;
#endif
public class DrawingBrushTests: TestBase
{
public DrawingBrushTests()
: base(@"Media\DrawingBrush")
{
}
[Fact]
public async Task DrawingBrushIsProperlyTiled()
{
Decorator target = new Decorator
{
Padding = new Thickness(10),
Width = 220,
Height = 220,
Child = new Rectangle
{
Fill = new DrawingBrush
{
Stretch = Stretch.None,
TileMode = TileMode.Tile,
Drawing = CreateDrawing(),
DestinationRect = new RelativeRect(0,0,0.25,0.25, RelativeUnit.Relative)
}
}
};
await RenderToFile(target);
CompareImages();
}
#if AVALONIA_SKIA
[Fact]
#endif
public async Task DrawingBrushIsProperlyUpscaled()
{
Decorator target = new Decorator
{
Padding = new Thickness(10),
Width = 420,
Height = 420,
Child = new Rectangle
{
Fill = new DrawingBrush
{
Stretch = Stretch.Fill,
TileMode = TileMode.None,
Drawing = CreateDrawing()
}
}
};
await RenderToFile(target);
CompareImages();
}
GeometryDrawing CreateDrawing()
{
return new GeometryDrawing
{
Geometry = new GeometryGroup
{
Children =
{
new RectangleGeometry(new Rect(50, 25, 25, 25)),
new RectangleGeometry(new Rect(25, 50, 25, 25)),
}
},
Pen = new Pen(new LinearGradientBrush()
{
GradientStops =
{
new GradientStop(Colors.Blue, 0),
new GradientStop(Colors.Black, 1),
}
}, 5),
Brush = Brushes.Yellow,
};
}
}

6
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -34,15 +34,15 @@ namespace Avalonia.UnitTests
} }
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) public IDrawingContextImpl CreateDrawingContext()
{ {
var m = new Mock<IDrawingContextImpl>(); var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>())) m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() => .Returns(() =>
{ {
var r = new Mock<IDrawingContextLayerImpl>(); var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())) r.Setup(r => r.CreateDrawingContext())
.Returns(CreateDrawingContext(null)); .Returns(CreateDrawingContext());
return r.Object; return r.Object;
} }
); );

4
tests/Avalonia.UnitTests/TestRoot.cs

@ -70,12 +70,12 @@ namespace Avalonia.UnitTests
{ {
var layerDc = new Mock<IDrawingContextImpl>(); var layerDc = new Mock<IDrawingContextImpl>();
var layer = new Mock<IDrawingContextLayerImpl>(); var layer = new Mock<IDrawingContextLayerImpl>();
layer.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(layerDc.Object); layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object);
return layer.Object; return layer.Object;
}); });
var result = new Mock<IRenderTarget>(); var result = new Mock<IRenderTarget>();
result.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(dc.Object); result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object);
return result.Object; return result.Object;
} }

BIN
tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Loading…
Cancel
Save