diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index bf27747154..4a3e20ff5b 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -8,22 +9,27 @@ using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Avalonia.Threading; +using Avalonia.Utilities; using SkiaSharp; namespace RenderDemo.Pages { public class CustomSkiaPage : Control { + private readonly GlyphRun _noSkia; public CustomSkiaPage() { 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 { - private readonly FormattedText _noSkia; + private readonly GlyphRun _noSkia; - public CustomDrawOp(Rect bounds, FormattedText noSkia) + public CustomDrawOp(Rect bounds, GlyphRun noSkia) { _noSkia = noSkia; Bounds = bounds; @@ -42,10 +48,7 @@ namespace RenderDemo.Pages { var leaseFeature = context.GetFeature(); if (leaseFeature == null) - using (var c = new DrawingContext(context, false)) - { - c.DrawText(_noSkia, new Point()); - } + context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); else { using var lease = leaseFeature.Lease(); @@ -114,10 +117,7 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black); - - context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); + context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia)); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); } } diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs index cc5125609c..2fe57165b3 100644 --- a/samples/RenderDemo/Pages/PathMeasurementPage.cs +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -37,11 +37,8 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using (var bitmapCtx = new DrawingContext(ctxi, false)) + using (var bitmapCtx = _bitmap.CreateDrawingContext()) { - ctxi.Clear(default); - var basePath = new PathGeometry(); using (var basePathCtx = basePath.Open()) diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index f365b59c20..b88dded39b 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -28,13 +28,11 @@ namespace RenderDemo.Pages readonly Stopwatch _st = Stopwatch.StartNew(); public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using(var ctx = new DrawingContext(ctxi, false)) + using (var ctx = _bitmap.CreateDrawingContext()) using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateTranslation(100, 100))) { - ctxi.Clear(default); ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs new file mode 100644 index 0000000000..2825628948 --- /dev/null +++ b/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 +{ + /// + /// Paints an area with an . + /// + public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + static DrawingBrush() + { + AffectsRender(DrawingProperty); + } + + /// + /// Initializes a new instance of the class. + /// + public DrawingBrush() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + public DrawingBrush(Drawing visual) + { + Drawing = visual; + } + + /// + /// Gets or sets the visual to draw. + /// + 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); + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index a37fa6fd32..31a16dc69c 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -8,83 +8,45 @@ using Avalonia.Media.Imaging; namespace Avalonia.Media { - public sealed class DrawingContext : IDisposable + public abstract class DrawingContext : IDisposable { - private readonly bool _ownsImpl; - private int _currentLevel; + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + private Stack? _states; - private static ThreadSafeObjectPool> StateStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private static ThreadSafeObjectPool> TransformStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _states = StateStackPool.Get(); - - private Stack? _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) + internal DrawingContext() { - 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; - - /// - /// Gets the current transform of the drawing context. - /// - public Matrix CurrentTransform + public void Dispose() { - get { return _currentTransform; } - private set + if (_states != null) { - _currentTransform = value; - var transform = _currentTransform * _currentContainerTransform; - PlatformImpl.Transform = transform; - } - } + while (_states.Count > 0) + _states.Pop().Dispose(); - //HACK: This is a temporary hack that is used in the render loop - //to update TransformedBounds property - [Obsolete("HACK for render loop, don't use")] - public Matrix CurrentContainerTransform => _currentContainerTransform; + StateStackPool.ReturnAndSetNull(ref _states); + } + DisposeCore(); + } + + protected abstract void DisposeCore(); + /// /// Draws an image. /// /// The image. /// The rect in the output to draw to. - public void DrawImage(IImage source, Rect rect) + public virtual void DrawImage(IImage source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - DrawImage(source, new Rect(source.Size), rect); } + /// /// Draws an image. /// @@ -92,12 +54,22 @@ namespace Avalonia.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - 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.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } + + /// + /// Draws a platform-specific bitmap impl. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a line. @@ -108,11 +80,11 @@ namespace Avalonia.Media public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) - { - PlatformImpl.DrawLine(pen, p1, p2); - } + DrawLineCore(pen, p1, p2); } + protected abstract void DrawLineCore(IPen pen, Point p1, Point p2); + /// /// Draws a geometry. /// @@ -121,10 +93,10 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) { - if (geometry.PlatformImpl is not null) - DrawGeometry(brush, pen, geometry.PlatformImpl); + if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null) + DrawGeometryCore(brush, pen, geometry.PlatformImpl); } - + /// /// Draws a geometry. /// @@ -133,14 +105,12 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - _ = geometry ?? throw new ArgumentNullException(nameof(geometry)); - - if (brush != null || PenIsVisible(pen)) - { - PlatformImpl.DrawGeometry(brush, pen, geometry); - } + if ((brush != null || PenIsVisible(pen))) + DrawGeometryCore(brush, pen, geometry); } + protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry); + /// /// Draws a rectangle with the specified Brush and Pen. /// @@ -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. /// 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. /// - 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) { if (brush == null && !PenIsVisible(pen)) - { return; - } - if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); @@ -175,20 +143,48 @@ namespace Avalonia.Media { 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); + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// Box shadow effect parameters + /// + /// 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. + /// + 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); + /// /// Draws the outline of a rectangle. /// /// The pen. /// The rectangle bounds. /// The corner radius. - 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); - } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); /// /// Draws an ellipse with the specified Brush and Pen. @@ -204,35 +200,50 @@ namespace Avalonia.Media /// 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; - var width = radiusX * 2; - var height = radiusY * 2; - - PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The bounding rect. + /// + /// 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. + /// + 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); + /// /// Draws a custom drawing operation /// /// custom operation - public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); + public abstract void Custom(ICustomDrawOperation custom); /// /// Draws text. /// /// The upper-left corner of the text. /// The text. - public void DrawText(FormattedText text, Point origin) + public virtual void DrawText(FormattedText text, Point origin) { _ = text ?? throw new ArgumentNullException(nameof(text)); - text.Draw(this, origin); + text.Draw(this, origin); } /// @@ -240,30 +251,31 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - 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; } - } - /// - /// Draws a filled rectangle. - /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - { - DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + public void Dispose() + { + if(_context?._states == null) + return; + if(_context._states.Count != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._states.Pop().Dispose(); + } } - - public readonly record struct PushedState : IDisposable + + private readonly record struct RestoreState : IDisposable { - private readonly int _level; private readonly DrawingContext _context; private readonly Matrix _matrix; private readonly PushedStateType _type; @@ -271,62 +283,56 @@ namespace Avalonia.Media public enum PushedStateType { None, - Matrix, + Transform, Opacity, Clip, - MatrixContainer, GeometryClip, 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; _type = type; - _matrix = matrix; - _level = context._currentLevel += 1; - context._states.Push(this); } public void Dispose() { if (_type == PushedStateType.None) return; - if (_context._states is null || _context._transformContainers is null) + if (_context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); - if (_context._currentLevel != _level) - throw new InvalidOperationException("Wrong Push/Pop state order"); - _context._currentLevel--; - _context._states.Pop(); - if (_type == PushedStateType.Matrix) - _context.CurrentTransform = _matrix; + if (_type == PushedStateType.Transform) + _context.PopTransformCore(); else if (_type == PushedStateType.Clip) - _context.PlatformImpl.PopClip(); + _context.PopClipCore(); else if (_type == PushedStateType.Opacity) - _context.PlatformImpl.PopOpacity(); + _context.PopOpacityCore(); else if (_type == PushedStateType.GeometryClip) - _context.PlatformImpl.PopGeometryClip(); + _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) - _context.PlatformImpl.PopOpacityMask(); - else if (_type == PushedStateType.MatrixContainer) - { - var cont = _context._transformContainers.Pop(); - _context._currentContainerTransform = cont.ContainerTransform; - _context.CurrentTransform = cont.LocalTransform; - } + _context.PopOpacityMaskCore(); + else if (_type == PushedStateType.BitmapBlendMode) + _context.PopBitmapBlendModeCore(); } } - + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. public PushedState PushClip(RoundedRect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + protected abstract void PushClipCore(RoundedRect rect); + /// /// Pushes a clip rectangle. /// @@ -334,9 +340,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + + protected abstract void PushClipCore(Rect rect); /// /// Pushes a clip geometry. @@ -345,17 +355,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { - _ = clip ?? throw new ArgumentNullException(nameof(clip)); - - // HACK: This check was added when nullable annotations pointed out that we're potentially - // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but - // 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); + PushGeometryClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); + return new PushedState(this); } + + protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. @@ -364,11 +370,13 @@ namespace Avalonia.Media /// The bounds. /// A disposable used to undo the opacity. public PushedState PushOpacity(double opacity, Rect bounds) - //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity, bounds); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -380,70 +388,53 @@ namespace Avalonia.Media /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { - PlatformImpl.PushOpacityMask(mask, bounds); - return new PushedState(this, PushedState.PushedStateType.OpacityMask); + PushOpacityMaskCore(mask, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask)); + return new PushedState(this); } + protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); - /// - /// Pushes a matrix post-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); - - /// - /// Pushes a matrix pre-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); - - /// - /// Sets the current matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushSetTransform(Matrix matrix) + public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) { - var oldMatrix = CurrentTransform; - CurrentTransform = matrix; - - return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + PushBitmapBlendMode(blendingMode); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); + return new PushedState(this); } - /// - /// Pushes a new transform context. - /// - /// A disposable used to undo the transformation. - 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); - } + protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); /// - /// Disposes of any resources held by the . + /// Pushes a matrix transformation. /// - public void Dispose() + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushTransform(Matrix matrix) { - if (_states is null || _transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - while (_states.Count != 0) - _states.Peek().Dispose(); - 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(); + PushTransformCore(matrix); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform)); + return new PushedState(this); } + [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) { return pen?.Brush != null && pen.Thickness > 0; diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 7b02649b6c..812d315912 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,10 +67,7 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { @@ -105,7 +102,7 @@ namespace Avalonia.Media return rect; } - private class DrawingGroupDrawingContext : IDrawingContextImpl + private sealed class DrawingGroupDrawingContext : DrawingContext { private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); @@ -135,17 +132,7 @@ namespace Avalonia.Media _drawingGroup = drawingGroup; } - public Matrix Transform - { - get => _transform; - set - { - _transform = value; - PushTransform(new MatrixTransform(value)); - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if ((brush == null) && (pen == null)) { @@ -159,7 +146,7 @@ namespace Avalonia.Media 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)) { @@ -169,7 +156,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground == null) { @@ -179,124 +166,70 @@ namespace Avalonia.Media GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = new GlyphRun(glyphRun) + GlyphRun = glyphRun }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen? pen, Point p1, Point p2) - { - 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) + protected override void PushClipCore(RoundedRect rect) { throw new NotImplementedException(); } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushClipCore(Rect rect) { throw new NotImplementedException(); } - public void Custom(ICustomDrawOperation custom) + protected override void PushGeometryClipCore(Geometry clip) { throw new NotImplementedException(); } - public object? GetFeature(Type t) => null; - - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + protected override void PushOpacityCore(double opacity, Rect bounds) { throw new NotImplementedException(); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { throw new NotImplementedException(); } - public void PopClip() + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); } - public void PopGeometryClip() + protected override void DrawLineCore(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); - } - - 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(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - public void PushClip(RoundedRect clip) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); } - 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) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -366,22 +299,34 @@ namespace Avalonia.Media // Restore the previous value of the current drawing group _currentDrawingGroup = _previousDrawingGroupStack.Pop(); } - + /// /// PushTransform - /// Push a Transform which will apply to all drawing operations until the corresponding /// Pop. /// - /// The Transform to push. - private void PushTransform(Transform transform) + /// The transform to push. + protected override void PushTransformCore(Matrix matrix) { // Instantiate a new drawing group and set it as the _currentDrawingGroup var drawingGroup = PushNewDrawingGroup(); // 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(); + /// /// Creates a new DrawingGroup for a Push* call by setting the /// _currentDrawingGroup to a newly instantiated DrawingGroup, diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 1b22a1ee69..52fbd87db7 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -62,7 +62,7 @@ namespace Avalonia.Media -sourceRect.Y + destRect.Y - bounds.Y); using (context.PushClip(destRect)) - using (context.PushPreTransform(translate * scale)) + using (context.PushTransform(translate * scale)) { Drawing?.Draw(context); } diff --git a/src/Avalonia.Base/Media/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs new file mode 100644 index 0000000000..df72dd1ace --- /dev/null +++ b/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) + { + } + } +} diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs deleted file mode 100644 index a7d3e4da10..0000000000 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - /// - /// Paints an area with an . - /// - [NotClientImplementable] - public interface IVisualBrush : ITileBrush - { - /// - /// Gets the visual to draw. - /// - Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 6577532891..c4720d772e 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { - context.PlatformImpl.DrawBitmap( + context.DrawBitmap( PlatformImpl, 1, sourceRect, diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 88e5e627ee..e77dd9d1ab 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget + public class RenderTargetBitmap : Bitmap, IDisposable { /// /// Initializes a new instance of the class. @@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging /// Renders a visual to the . /// /// The visual to render. - public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); + public void Render(Visual visual) + { + using (var ctx = CreateDrawingContext()) + ImmediateRenderer.Render(visual, ctx); + } /// /// Creates a platform-specific implementation for a . @@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging return factory.CreateRenderTargetBitmap(size, dpi); } - /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); - - bool IRenderTarget.IsCorrupted => false; + public DrawingContext CreateDrawingContext() + { + var platform = PlatformImpl.Item.CreateDrawingContext(); + platform.Clear(Colors.Transparent); + return new PlatformDrawingContext(platform); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 2564d89bac..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -354,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index e9086eee37..0000000000 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media.Imaging; - -namespace Avalonia.Media.Immutable -{ - /// - /// Paints an area with an . - /// - internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush - { - /// - /// Initializes a new instance of the class. - /// - /// The visual to draw. - /// The horizontal alignment of a tile in the destination. - /// The vertical alignment of a tile in the destination. - /// The rectangle on the destination in which to paint a tile. - /// The opacity of the brush. - /// The transform of the brush. - /// The transform origin of the brush - /// The rectangle of the source image that will be displayed. - /// - /// How the source rectangle will be stretched to fill the destination rect. - /// - /// The tile mode. - /// Controls the quality of interpolation. - 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; - } - - /// - /// Initializes a new instance of the class. - /// - /// The brush from which this brush's properties should be copied. - public ImmutableVisualBrush(IVisualBrush source) - : base(source) - { - Visual = source.Visual; - } - - /// - public Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs new file mode 100644 index 0000000000..eb8a93722c --- /dev/null +++ b/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> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _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 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); + } +} diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 2be3e9a94e..6bfe20271f 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,11 +1,14 @@ using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush + public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender { /// /// Defines the property. @@ -43,10 +46,23 @@ namespace Avalonia.Media set { SetValue(VisualProperty, value); } } - /// - IImmutableBrush IMutableBrush.ToImmutable() + ISceneBrushContent? ISceneBrush.CreateContent() { - 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); } } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 73e9e58da4..31ad84341d 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -14,11 +14,7 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// - /// A render to be used to render visual brushes. May be null if no visual brushes are - /// to be drawn. - /// - IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); + IDrawingContextImpl CreateDrawingContext(); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 7fa2d4955f..01299e4ffa 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor { _root = root; _compositor = compositor; - _recordingContext = new DrawingContext(_recorder); + _recordingContext = _recorder; CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index 10a7c3e360..5d45a725c1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing; /// internal class CompositionDrawList : PooledList> { - public Size? Size { get; set; } - public CompositionDrawList() { @@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count) { Size = Size }; + var clone = new CompositionDrawList(Count); foreach (var r in this) clone.Add(r.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) { - canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + if (cmd.Item is IDrawOperationWithTransform hasTransform) + canvas.Transform = hasTransform.Transform * transform; 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; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs new file mode 100644 index 0000000000..85bb156475 --- /dev/null +++ b/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; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 6b380608fe..f81cc5a1a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -7,7 +8,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.VisualTree; +using Avalonia.Threading; // Special license applies License.md @@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition; /// /// An IDrawingContextImpl implementation that builds /// -internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport { private CompositionDrawListBuilder _builder = new(); private int _drawOperationIndex; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public Matrix Transform { get; set; } = Matrix.Identity; + private Stack? _transforms; - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } + private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } + private Stack? _needsToPopOpacityMask; + public Matrix Transform { get; set; } = Matrix.Identity; + public void BeginUpdate(CompositionDrawList? list) { _builder.Reset(list); _drawOperationIndex = 0; } - public CompositionDrawList EndUpdate() + public CompositionDrawList? EndUpdate() { + // Make sure that any pending pop operations are completed + Dispose(); + _builder.TrimTo(_drawOperationIndex); - return _builder.DrawOperations!; + return _builder.DrawOperations; } + + protected override void DisposeCore() + { + if (_transforms != null) + { + _transforms.Clear(); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + if (_needsToPopOpacityMask != null) + { + _needsToPopOpacityMask.Clear(); + _needsToPopOpacityMask = null; + } + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { var next = NextDrawAs(); 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 { @@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { var next = NextDrawAs(); @@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen? pen, Point p1, Point p2) + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) { if (pen is null) { @@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW 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 { @@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { var next = NextDrawAs(); 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 { @@ -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(); 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 { ++_drawOperationIndex; } } - - public void Custom(ICustomDrawOperation custom) + + public override void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, custom)) @@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } - public object? GetFeature(Type t) => null; - - /// - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground is null) { @@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW var next = NextDrawAs(); - 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 @@ -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(); - /// - public void PopClip() + protected override void PopClipCore() { var next = NextDrawAs(); @@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PopGeometryClip() + protected override void PopGeometryClipCore() { var next = NextDrawAs(); @@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopBitmapBlendMode() + protected override void PopBitmapBlendModeCore() { var next = NextDrawAs(); @@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacity() + protected override void PopOpacityCore() { var next = NextDrawAs(); @@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacityMask() + protected override void PopOpacityMaskCore() { + if (!_needsToPopOpacityMask!.Pop()) + return; + var next = NextDrawAs(); if (next == null || !next.Item.Equals(null, null)) { - Add(new OpacityMaskNode()); + Add(new OpacityMaskPopNode()); } else { @@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(Rect clip) + + protected override void PushClipCore(Rect clip) { var next = NextDrawAs(); @@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(RoundedRect clip) + protected override void PushClipCore(RoundedRect clip) { var next = NextDrawAs(); @@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushGeometryClip(IGeometryImpl? clip) + protected override void PushGeometryClipCore(Geometry clip) { - if (clip is null) + if (clip.PlatformImpl is null) return; var next = NextDrawAs(); - 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 { ++_drawOperationIndex; } } - - /// - public void PushOpacity(double opacity, Rect bounds) + + protected override void PushOpacityCore(double opacity, Rect bounds) { var next = NextDrawAs(); @@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushOpacityMask(IBrush mask, Rect bounds) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { var next = NextDrawAs(); + bool needsToPop = true; 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 { ++_drawOperationIndex; } + + _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get(); + _needsToPopOpacityMask.Push(needsToPop); } /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { var next = NextDrawAs(); @@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW : null; } - private static IDisposable? CreateChildScene(IBrush? brush) + private IImmutableBrush? ConvertBrush(IBrush? brush) { - if (brush is VisualBrush visualBrush) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - // 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; + if (brush is IMutableBrush mutable) + return mutable.ToImmutable(); + if (brush is ISceneBrush sceneBrush) + return sceneBrush.CreateContent(); + return (IImmutableBrush?)brush; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 08e506536f..eaa9a70ca0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDrawingContextImpl _impl; - private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) + public CompositorDrawingContextProxy(IDrawingContextImpl 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; @@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont } 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) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 63ec8d756b..977acd8470 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server Readback.CompleteWrite(Revision); _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext(null)) + using (var targetContext = _renderTarget.CreateDrawingContext()) { var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) @@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsDefault) { - var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); - using (var context = _layer.CreateDrawingContext(visualBrushHelper)) + using (var context = _layer.CreateDrawingContext()) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); + Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index f5312ad39b..0000000000 --- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - [Unstable] - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 09d2d55ce3..4a12e78817 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -14,19 +14,8 @@ namespace Avalonia.Rendering /// a simple tree traversal. /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer + internal class ImmediateRenderer { - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(Visual visual, IRenderTarget target) - { - using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); - Render(context, visual, visual.Bounds); - } - /// /// Renders a visual to a drawing context. /// @@ -36,28 +25,6 @@ namespace Avalonia.Rendering { Render(context, visual, visual.Bounds); } - - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? default; - } - - /// - 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) { @@ -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 clipToBounds = visual.ClipToBounds; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index e81966ce81..62fc73db44 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - 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) { - Aux = aux; + Brush = brush; } - /// - /// Auxiliary data required to draw the brush - /// - public IDisposable? Aux { get; } - public override void Dispose() { - Aux?.Dispose(); + (Brush as ISceneBrushContent)?.Dispose(); base.Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index e1bfaa4aa3..782e287989 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a clip push or pop. /// - internal class ClipNode : IDrawOperation + internal class ClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index b7311936d3..ff2616bfe4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -4,30 +4,19 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperation + internal sealed class CustomDrawOperation : DrawOperationWithTransform { - public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) : base(custom.Bounds, transform) { - Transform = transform; Custom = custom; } - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - return Custom.HitTest(p * Transform.Invert()); - } - - return false; - } + public override bool HitTest(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; Custom.Render(context); } @@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph Transform == transform && Custom?.Equals(custom) == true; } - public interface ICustomDrawOperation : IDrawOperation, IEquatable + public interface ICustomDrawOperation : IEquatable, IDisposable { - + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index c49e7705e0..5b93cd8cfc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/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; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index 4600653b9d..d5f0270cb2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph { public EllipseNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - Rect rect, - IDisposable? aux = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) + Rect rect) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; } - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// public ImmutablePen? Pen { get; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the rect of the ellipse to draw. /// @@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph rect.Equals(Rect); } - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawEllipse(Brush, Pen, Rect); - } + public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); public override bool HitTest(Point p) { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - var center = Rect.Center; var strokeThickness = Pen?.Thickness ?? 0; @@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph return false; } + + public override void Dispose() + { + (Brush as ISceneBrushContent)?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 82f8fc2d56..e1f79e0e10 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a rectangle draw. /// - internal class ExperimentalAcrylicNode : DrawOperation + internal class ExperimentalAcrylicNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect) : base(rect.Rect, transform) { - Transform = transform; Material = material.ToImmutable(); Rect = rect; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - public IExperimentalAcrylicMaterial Material { get; } /// @@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if(context is IDrawingContextWithAcrylicLikeSupport idc) { idc.DrawRectangle(Material, Rect); @@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) - { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) - { - p *= Transform.Invert(); - - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } - - return false; - } + public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 842edf2bcb..8575e61de4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a geometry clip push or pop. /// - internal class GeometryClipNode : IDrawOperation + internal class GeometryClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index cf53b86fa7..3ab535897a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph /// The geometry. /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - IGeometryImpl geometry, - IDisposable? aux) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) + IGeometryImpl geometry) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } /// public override bool HitTest(Point p) { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index a2d914bdd7..4d8759f545 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, - IBrush foreground, - IRef glyphRun, - IDisposable? aux = null) - : base(new Rect(glyphRun.Item.Size), transform, aux) + IImmutableBrush foreground, + IRef glyphRun) + : base(new Rect(glyphRun.Item.Size), transform, foreground) { - Transform = transform; - Foreground = foreground.ToImmutable(); GlyphRun = glyphRun.Clone(); } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the foreground brush. - /// - public IBrush Foreground { get; } - + + /// /// Gets the glyph run to draw. /// public IRef GlyphRun { get; } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun); - } + public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); /// /// Determines if this draw operation equals another. @@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && - Equals(foreground, Foreground) && + Equals(foreground, Brush) && Equals(glyphRun.Item, GlyphRun.Item); } /// - 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() { GlyphRun?.Dispose(); + base.Dispose(); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 2bfd2080c3..6a1aefe6b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation : IDisposable + internal interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. @@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph /// The drawing context. void Render(IDrawingContextImpl context); } + + internal interface IDrawOperationWithTransform : IDrawOperation + { + /// + /// Gets the transform with which the node will be drawn. + /// + Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 339881e675..dd9787e8d1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : DrawOperation + internal class ImageNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform) { - Transform = transform; Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } + } /// /// Gets the image to draw. @@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BitmapInterpolationMode BitmapInterpolationMode { get; } - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - /// /// Determines if this draw operation equals another. /// @@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 0af8ba2752..f21791d038 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a line draw. /// - internal class LineNode : BrushDrawOperation + internal class LineNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IPen pen, Point p1, - Point p2, - IDisposable? aux = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) + Point p2) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { - Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the stroke pen. /// @@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawLine(Pen, P1, P2); } public override bool HitTest(Point p) { - if (!Transform.HasInverse) - return false; - - p *= Transform.Invert(); - var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 3ecc07fa54..e10d712c2d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Auxiliary data required to draw the brush. - public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(default, Matrix.Identity, aux) + public OpacityMaskNode(IImmutableBrush mask, Rect bounds) + : base(default, Matrix.Identity, mask) { - Mask = mask.ToImmutable(); MaskBounds = bounds; } - /// - /// Initializes a new instance of the class that represents an - /// opacity mask pop. - /// - public OpacityMaskNode() - : base(default, Matrix.Identity, null) - { - } - - /// - /// Gets the mask to be pushed or null if the operation represents a pop. - /// - public IBrush? Mask { get; } - /// /// Gets the bounds of the opacity mask or null if the operation represents a pop. /// @@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; + public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; /// public override void Render(IDrawingContextImpl context) { - if (Mask != null) - { - context.PushOpacityMask(Mask, MaskBounds!.Value); - } - else - { - context.PopOpacityMask(); - } + context.PushOpacityMask(Brush!, MaskBounds!.Value); } } + + 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(); + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index f2ffd7411c..cee9ce9df7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows, - IDisposable? aux = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) + BoxShadows boxShadows) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; BoxShadows = boxShadows; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - - context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - } + public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// public override bool HitTest(Point p) { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) + if (Brush != null) { - p *= Transform.Invert(); - - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - 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); - } + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + 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; } } } diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 827a02334a..30b7738409 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Avalonia.Threading { - public class ThreadSafeObjectPool where T : class, new() + internal class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); @@ -17,11 +17,14 @@ namespace Avalonia.Threading } } - public void Return(T obj) + public void ReturnAndSetNull(ref T? obj) { + if (obj == null) + return; lock (_stack) { _stack.Push(obj); + obj = null; } } } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e1f840672d..dbffb803a3 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls public sealed override void Render(DrawingContext context) { - if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + if (context is IDrawingContextWithAcrylicLikeSupport idc) { var cornerRadius = CornerRadius; diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 6239a5120d..799cc47d0c 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Utils var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); - context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); + context.DrawRectangle(background, pen, rrect, boxShadows); } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5b84ceef7f..31aaebcdc7 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -325,7 +325,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } @@ -491,7 +491,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 56fd2f14ef..13068832fb 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -115,7 +115,7 @@ namespace Avalonia.X11 using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) 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)); ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 51db815b31..84a1d35712 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 _bdata = new uint[_width * _height]; using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) 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), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 969f0b5e2a..db7b068543 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Skia.Helpers; using SkiaSharp; +using ISceneBrush = Avalonia.Media.ISceneBrush; namespace Avalonia.Skia { @@ -25,7 +27,6 @@ namespace Avalonia.Skia private readonly Stack _opacityStack = new(); private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer? _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; @@ -61,12 +62,7 @@ namespace Avalonia.Skia /// Dpi of drawings. /// public Vector Dpi; - - /// - /// Visual brush renderer. - /// - public IVisualBrushRenderer? VisualBrushRenderer; - + /// /// Render text without Lcd rendering. /// @@ -141,7 +137,6 @@ namespace Avalonia.Skia ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); _dpi = createInfo.Dpi; - _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; @@ -908,7 +903,7 @@ namespace Avalonia.Skia paintWrapper.AddDisposable(intermediate); - using (var context = intermediate.CreateDrawingContext(null)) + using (var context = intermediate.CreateDrawingContext()) { var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); @@ -970,36 +965,98 @@ namespace Avalonia.Skia } } - /// - /// Configure paint wrapper to use visual brush. - /// - /// Paint wrapper. - /// Visual brush. - /// Visual brush renderer. - /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, - IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage) + private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) { - if (visualBrushRenderer == null) - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } - - var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); + if(content.UseScalableRasterization) + ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize); + else + ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize); + } + + 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) { - 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); - - visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } - tileBrushImage = intermediate; - paintWrapper.AddDisposable(tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate); + } + } + + 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 visualBrush = brush as IVisualBrush; 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 { diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index f1216100bc..a22b67e09e 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -36,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -55,7 +55,6 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true }; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 7f9108481d..797c565ca1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var session = _renderTarget.BeginRenderingSession(); @@ -31,7 +31,6 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, Gpu = _skiaGpu, CurrentSession = session diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 4d8afe9830..ec24f8f624 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -15,13 +15,12 @@ namespace Avalonia.Skia.Helpers /// /// /// DrawingContext - public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null) + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi) { var createInfo = new DrawingContextImpl.CreateInfo { Canvas = canvas, Dpi = dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, }; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index 4cb1430a3b..6adfc01951 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/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); + } } } diff --git a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs new file mode 100644 index 0000000000..280b7c27cd --- /dev/null +++ b/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(); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index f88e74d738..92210c30e2 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -97,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -106,7 +106,6 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 02932c52da..10f9239b1a 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => + return new DrawingContextImpl( null, target, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 984a24fb30..0af326d6a8 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/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(); if (locked.Format == PixelFormat.Rgb565) @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(visualBrushRenderer); + .CreateDrawingContext(); } public bool IsCorrupted => false; @@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1 _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)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 0dd9c155bb..f9b5953e3f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media /// internal class DrawingContextImpl : IDrawingContextImpl { - private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; @@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( - IVisualBrushRenderer visualBrushRenderer, ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { - _visualBrushRenderer = visualBrushRenderer; _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; @@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media var radialGradientBrush = brush as IRadialGradientBrush; var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; - var visualBrush = brush as IVisualBrush; + var sceneBrush = brush as ISceneBrush; + var sceneBrushContent = brush as ISceneBrushContent; if (solidColorBrush != null) { @@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media (BitmapImpl)imageBrush.Source.PlatformImpl.Item, 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) { @@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); using (var intermediate = new BitmapRenderTarget( - _deviceContext, - CompatibleRenderTargetOptions.None, - pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) + _deviceContext, + CompatibleRenderTargetOptions.None, + pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + sceneBrushContent.Render(ctx, + rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } return new ImageBrushImpl( - visualBrush, + sceneBrushContent.Brush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), destinationSize); } + } } - else - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } } return new SolidColorBrushImpl(null, _deviceContext); diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 829b887d9d..a08c96c40c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, 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 rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2dbc1d67d1..6b1ca911fb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging 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; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index d6b1e618e5..fa40e75fa7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - => CreateDrawingContext(visualBrushRenderer, null); + public virtual IDrawingContextImpl CreateDrawingContext() + => CreateDrawingContext(null); 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++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 8d5062336c..4392e35058 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); + return new DrawingContextImpl(this, _renderTarget); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 531c4119af..385120505c 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var size = GetWindowSize(); var dpi = GetWindowDpi(); @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 Resize(); } - return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); + return new DrawingContextImpl(this, _deviceContext, _swapChain); } public bool IsCorrupted => false; diff --git a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs index d75bf9fe8c..f91b4b613c 100644 --- a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs +++ b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests private DrawingContext CreateDrawingContext() { - return new DrawingContext(Mock.Of()); + return new PlatformDrawingContext(Mock.Of()); } private class TestControl : Control diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index c1468a28e4..9d810fa110 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph new Matrix(), Brushes.Black, null, - geometry, default); + geometry); geometryNode.HitTest(new Point()); } @@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph private class TestRectangleDrawOperation : RectangleNode { 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()) { } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs index 565b217180..a2e438e3e0 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] 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); @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] 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); diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs index b0db806afa..2905b1e464 100644 --- a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering _lineFill = new Line { 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().ToConstant(new NullRenderingPlatform()); } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 05e160dca8..4ba0c82b87 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -76,7 +76,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media var r = Avalonia.AvaloniaLocator.Current.GetRequiredService(); using(var cpuContext = r.CreateBackendContext(null)) using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) - using (var ctx = target.CreateDrawingContext(null)) + using (var ctx = target.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); @@ -90,7 +90,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media fb.Deallocate(); 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.Pink, null, new Rect(0, 20, 100, 10)); diff --git a/tests/Avalonia.RenderTests/Media/TileBrushTests.cs b/tests/Avalonia.RenderTests/Media/TileBrushTests.cs new file mode 100644 index 0000000000..c171573be7 --- /dev/null +++ b/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, + }; + } + +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index fe84659038..df128b8ae3 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -34,15 +34,15 @@ namespace Avalonia.UnitTests } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var m = new Mock(); m.Setup(c => c.CreateLayer(It.IsAny())) .Returns(() => { var r = new Mock(); - r.Setup(r => r.CreateDrawingContext(It.IsAny())) - .Returns(CreateDrawingContext(null)); + r.Setup(r => r.CreateDrawingContext()) + .Returns(CreateDrawingContext()); return r.Object; } ); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 875c5eb944..c17eeda3e1 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -70,12 +70,12 @@ namespace Avalonia.UnitTests { var layerDc = new Mock(); var layer = new Mock(); - layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); + layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object); return layer.Object; }); var result = new Mock(); - result.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(dc.Object); + result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object); return result.Object; } diff --git a/tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png b/tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png new file mode 100644 index 0000000000..78b6764360 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png differ diff --git a/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png b/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png new file mode 100644 index 0000000000..1187581a92 Binary files /dev/null and b/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png differ diff --git a/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png b/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png new file mode 100644 index 0000000000..910e38ca1b Binary files /dev/null and b/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png differ