diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 59a895a22f..f56e7448a7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -12,20 +11,16 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : IDrawOperation + internal abstract class BrushDrawOperation : DrawOperation { - /// - public abstract Rect Bounds { get; } - - /// - public abstract bool HitTest(Point p); + public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen) + : base(bounds, transform, pen) + { + } /// /// Gets a collection of child scenes that are needed to draw visual brushes. /// public abstract IDictionary ChildScenes { get; } - - /// - public abstract void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs new file mode 100644 index 0000000000..4c6ed189ff --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// Base class for draw operations that have bounds. + /// + internal abstract class DrawOperation : IDrawOperation + { + public DrawOperation(Rect bounds, Matrix transform, Pen pen) + { + bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); + Bounds = new Rect( + new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), + new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); + } + + public Rect Bounds { get; } + + public abstract bool HitTest(Point p); + + public abstract void Render(IDrawingContextImpl context); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index b884c42d99..6310122183 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Pen pen, IGeometryImpl geometry, IDictionary childScenes = null) + : base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null) { - Bounds = geometry.GetRenderBounds(pen?.Thickness ?? 0).TransformToAABB(transform); Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs index 8c3bb72463..839fd9b0e5 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph public interface IDrawOperation { /// - /// Gets the bounds of the visible content in the node. + /// Gets the bounds of the visible content in the node in global coordinates. /// Rect Bounds { get; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 4a50f12095..8291d1c0bb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : IDrawOperation + internal class ImageNode : DrawOperation { /// /// Initializes a new instance of the class. @@ -20,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph /// The source rect. /// The destination rect. public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + : base(destRect, transform, null) { - Bounds = destRect.TransformToAABB(transform); Transform = transform; Source = source; Opacity = opacity; @@ -29,9 +29,6 @@ namespace Avalonia.Rendering.SceneGraph DestRect = destRect; } - /// - public Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// @@ -80,7 +77,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void Render(IDrawingContextImpl context) + public override void Render(IDrawingContextImpl context) { // TODO: Probably need to introduce some kind of locking mechanism in the case of // WriteableBitmap. @@ -89,6 +86,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public bool HitTest(Point p) => Bounds.Contains(p); + public override bool HitTest(Point p) => Bounds.Contains(p); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index e39335b5b6..d3df478a63 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Point p1, Point p2, IDictionary childScenes = null) + : base(new Rect(p1, p2), transform, pen) { - Bounds = new Rect(p1, p2).TransformToAABB(transform).Inflate(pen?.Thickness ?? 0); Transform = transform; Pen = pen?.ToImmutable(); P1 = p1; @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs index c40869724f..28b8f53e26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,6 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Child scenes for drawing visual brushes. public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary childScenes = null) + : base(Rect.Empty, Matrix.Identity, null) { Mask = mask?.ToImmutable(); MaskBounds = bounds; @@ -30,12 +31,10 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() + : base(Rect.Empty, Matrix.Identity, null) { } - /// - public override Rect Bounds => Rect.Empty; - /// /// Gets the mask to be pushed or null if the operation represents a pop. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 2affc454b5..1730621c55 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -30,8 +30,8 @@ namespace Avalonia.Rendering.SceneGraph Rect rect, float cornerRadius, IDictionary childScenes = null) + : base(rect, transform, pen) { - Bounds = rect.TransformToAABB(transform).Inflate(pen?.Thickness ?? 0); Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); @@ -40,9 +40,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 058f3b1c22..6328d7dd14 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Point origin, IFormattedTextImpl text, IDictionary childScenes = null) + : base(new Rect(origin, text.Size), transform, null) { - Bounds = new Rect(origin, text.Size).TransformToAABB(transform); Transform = transform; Foreground = foreground?.ToImmutable(); Origin = origin; @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs new file mode 100644 index 0000000000..76fe103c1b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class DrawOperationTests + { + [Fact] + public void Empty_Bounds_Remain_Empty() + { + var target = new TestDrawOperation(Rect.Empty, Matrix.Identity, null); + + Assert.Equal(Rect.Empty, target.Bounds); + } + + [Theory] + [InlineData(10, 10, 10, 10, 1, 1, 1, 9, 9, 12, 12)] + [InlineData(10, 10, 10, 10, 1, 1, 2, 9, 9, 12, 12)] + [InlineData(10, 10, 10, 10, 1.5, 1.5, 1, 14, 14, 17, 17)] + public void Rectangle_Bounds_Are_Snapped_To_Pixels( + double x, + double y, + double width, + double height, + double scaleX, + double scaleY, + double? penThickness, + double expectedX, + double expectedY, + double expectedWidth, + double expectedHeight) + { + var target = new TestDrawOperation( + new Rect(x, y, width, height), + Matrix.CreateScale(scaleX, scaleY), + penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null); + Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); + } + + private class TestDrawOperation : DrawOperation + { + public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) + :base(bounds, transform, pen) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) { } + } + } +}