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.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
index 1731950222..58f3413780 100644
--- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
@@ -104,7 +104,12 @@ namespace Avalonia.Collections
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
Remove(e.OldStartingIndex, e.OldItems);
- Add(e.NewStartingIndex, e.NewItems);
+ int newIndex = e.NewStartingIndex;
+ if(newIndex > e.OldStartingIndex)
+ {
+ newIndex -= e.OldItems.Count;
+ }
+ Add(newIndex, e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
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.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
index b996db8d48..7f118a2c1d 100644
--- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Linq;
+using System.Linq;
using Avalonia.Collections;
using Xunit;
@@ -82,13 +81,31 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(source, result);
}
- [Fact]
- public void CreateDerivedList_Handles_MoveRange()
+ [Theory]
+ [InlineData(0, 2, 3)]
+ [InlineData(0, 2, 4)]
+ [InlineData(0, 2, 5)]
+ [InlineData(0, 4, 4)]
+ [InlineData(1, 2, 0)]
+ [InlineData(1, 2, 4)]
+ [InlineData(1, 2, 5)]
+ [InlineData(1, 4, 0)]
+ [InlineData(2, 2, 0)]
+ [InlineData(2, 2, 1)]
+ [InlineData(2, 2, 3)]
+ [InlineData(2, 2, 4)]
+ [InlineData(2, 2, 5)]
+ [InlineData(4, 2, 0)]
+ [InlineData(4, 2, 1)]
+ [InlineData(4, 2, 3)]
+ [InlineData(5, 1, 0)]
+ [InlineData(5, 1, 3)]
+ public void CreateDerivedList_Handles_MoveRange(int oldIndex, int count, int newIndex)
{
- var source = new AvaloniaList(new[] { 0, 1, 2, 3 });
+ var source = new AvaloniaList(new[] { 0, 1, 2, 3, 4, 5 });
var target = source.CreateDerivedList(x => new Wrapper(x));
- source.MoveRange(1, 2, 0);
+ source.MoveRange(oldIndex, count, newIndex);
var result = target.Select(x => x.Value).ToList();
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);
+ }
}
}