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) { }
+ }
+ }
+}