diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
index b428cd1b9f..f7e1c08cac 100644
--- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
+++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
@@ -19,7 +19,7 @@
- Layout Transform
+
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index d373e7ef2a..08f3803e9b 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -125,6 +125,20 @@ namespace Avalonia.Rendering
if (m.HasValue)
{
var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value);
+
+ //use transformedbounds as previous render state of the visual bounds
+ //so we can invalidate old and new bounds of a control in case it moved/shrinked
+ if (visual.TransformedBounds.HasValue)
+ {
+ var trb = visual.TransformedBounds.Value;
+ var trBounds = trb.Bounds.TransformToAABB(trb.Transform);
+
+ if (trBounds != bounds)
+ {
+ _renderRoot?.Invalidate(trBounds);
+ }
+ }
+
_renderRoot?.Invalidate(bounds);
}
}
@@ -191,7 +205,7 @@ namespace Avalonia.Rendering
}
}
- static IEnumerable HitTest(
+ private static IEnumerable HitTest(
IVisual visual,
Point p,
Func filter)
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index e5fcf1ba1d..f26c21d1b6 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -304,7 +304,7 @@ namespace Avalonia
{
var thisOffset = GetOffsetFrom(common, this);
var thatOffset = GetOffsetFrom(common, visual);
- return Matrix.CreateTranslation(-thatOffset) * Matrix.CreateTranslation(thisOffset);
+ return -thatOffset * thisOffset;
}
return null;
@@ -454,13 +454,28 @@ namespace Avalonia
/// The ancestor visual.
/// The visual.
/// The visual offset.
- private static Vector GetOffsetFrom(IVisual ancestor, IVisual visual)
+ private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual)
{
- var result = new Vector();
+ var result = Matrix.Identity;
while (visual != ancestor)
{
- result = new Vector(result.X + visual.Bounds.X, result.Y + visual.Bounds.Y);
+ if (visual.RenderTransform?.Value != null)
+ {
+ var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size);
+ var offset = Matrix.CreateTranslation(origin);
+ var renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
+
+ result *= renderTransform;
+ }
+
+ var topLeft = visual.Bounds.TopLeft;
+
+ if (topLeft != default)
+ {
+ result *= Matrix.CreateTranslation(topLeft);
+ }
+
visual = visual.VisualParent;
if (visual == null)
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs
new file mode 100644
index 0000000000..82294246b1
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering
+{
+ public class ImmediateRendererTests
+ {
+ [Fact]
+ public void AddDirty_Call_RenderRoot_Invalidate()
+ {
+ var visual = new Mock();
+ var child = new Mock() { CallBase = true };
+ var renderRoot = visual.As();
+
+ visual.As().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+
+ child.As().Setup(v => v.Bounds).Returns(new Rect(10, 10, 100, 100));
+ child.As().Setup(v => v.VisualParent).Returns(visual.Object);
+
+ var target = new ImmediateRenderer(visual.Object);
+
+ target.AddDirty(child.Object);
+
+ renderRoot.Verify(v => v.Invalidate(new Rect(10, 10, 100, 100)));
+ }
+
+ [Fact]
+ public void AddDirty_With_RenderTransform_Call_RenderRoot_Invalidate()
+ {
+ var visual = new Mock();
+ var child = new Mock() { CallBase = true };
+ var renderRoot = visual.As();
+
+ visual.As().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+
+ child.As().Setup(v => v.Bounds).Returns(new Rect(100, 100, 100, 100));
+ child.As().Setup(v => v.VisualParent).Returns(visual.Object);
+ child.Object.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 };
+
+ var target = new ImmediateRenderer(visual.Object);
+
+ target.AddDirty(child.Object);
+
+ renderRoot.Verify(v => v.Invalidate(new Rect(50, 50, 200, 200)));
+ }
+
+ [Fact]
+ public void AddDirty_For_Child_Moved_Should_Invalidate_Previous_Bounds()
+ {
+ var visual = new Mock() { CallBase = true };
+ var child = new Mock() { CallBase = true };
+ var renderRoot = visual.As();
+ var renderTarget = visual.As();
+
+ renderRoot.Setup(r => r.CreateRenderTarget()).Returns(renderTarget.Object);
+ renderTarget.Setup(r => r.CreateDrawingContext(It.IsAny())).Returns(Mock.Of());
+
+ visual.As().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+ visual.As().Setup(v => v.VisualChildren).Returns(new AvaloniaList() { child.As().Object });
+
+ Rect childBounds = new Rect(0, 0, 100, 100);
+ child.As().Setup(v => v.Bounds).Returns(() => childBounds);
+ child.As().Setup(v => v.VisualParent).Returns(visual.Object);
+ child.As().Setup(v => v.VisualChildren).Returns(new AvaloniaList());
+
+ var invalidationCalls = new List();
+
+ renderRoot.Setup(v => v.Invalidate(It.IsAny())).Callback(v => invalidationCalls.Add(v));
+
+ var target = new ImmediateRenderer(visual.Object);
+
+ target.AddDirty(child.Object);
+
+ Assert.Equal(new Rect(0, 0, 100, 100), invalidationCalls[0]);
+
+ target.Paint(new Rect(0, 0, 100, 100));
+
+ //move child 100 pixels bottom/right
+ childBounds = new Rect(100, 100, 100, 100);
+
+ //renderer should invalidate old child bounds with new one
+ //as on old area there can be artifacts
+ target.AddDirty(child.Object);
+
+ //invalidate first old position
+ Assert.Equal(new Rect(0, 0, 100, 100), invalidationCalls[1]);
+
+ //then new position
+ Assert.Equal(new Rect(100, 100, 100, 100), invalidationCalls[2]);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
index b62bf5858d..0414ac4c74 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
+using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -192,5 +193,48 @@ namespace Avalonia.Visuals.UnitTests
Assert.Throws(() => root2.Child = child);
Assert.Empty(root2.GetVisualChildren());
}
+
+ [Fact]
+ public void TransformToVisual_Should_Work()
+ {
+ var child = new Decorator { Width = 100, Height = 100 };
+ var root = new TestRoot() { Child = child, Width = 400, Height = 400 };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(new Point(), root.DesiredSize));
+
+ var tr = child.TransformToVisual(root);
+
+ Assert.NotNull(tr);
+
+ var point = root.Bounds.TopLeft * tr;
+
+ //child is centered (400 - 100)/2
+ Assert.Equal(new Point(150, 150), point);
+ }
+
+ [Fact]
+ public void TransformToVisual_With_RenderTransform_Should_Work()
+ {
+ var child = new Decorator
+ {
+ Width = 100,
+ Height = 100,
+ RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }
+ };
+ var root = new TestRoot() { Child = child, Width = 400, Height = 400 };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(new Point(), root.DesiredSize));
+
+ var tr = child.TransformToVisual(root);
+
+ Assert.NotNull(tr);
+
+ var point = root.Bounds.TopLeft * tr;
+
+ //child is centered (400 - 100*2 scale)/2
+ Assert.Equal(new Point(100, 100), point);
+ }
}
}