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