diff --git a/parameters.cake b/parameters.cake
index d13e503bd0..4ef7e8e05a 100644
--- a/parameters.cake
+++ b/parameters.cake
@@ -64,7 +64,7 @@ public class Parameters
IsPullRequest = buildSystem.AppVeyor.Environment.PullRequest.IsPullRequest;
IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, context.EnvironmentVariable("BUILD_REPOSITORY_URI"));
IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals(MasterBranch, context.EnvironmentVariable("BUILD_SOURCEBRANCHNAME"));
- IsReleaseBranch = context.EnvironmentVariable("BUILD_SOURCEBRANCH").ToLower().StartsWith(ReleaseBranchPrefix.ToLower());
+ IsReleaseBranch = (context.EnvironmentVariable("BUILD_SOURCEBRANCH")??"").StartsWith(ReleaseBranchPrefix, StringComparison.OrdinalIgnoreCase);
IsTagged = buildSystem.AppVeyor.Environment.Repository.Tag.IsTag
&& !string.IsNullOrWhiteSpace(buildSystem.AppVeyor.Environment.Repository.Tag.Name);
IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration);
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.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index 24b2af7996..1f3fcbafb3 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -2,10 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Linq;
using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -251,7 +253,10 @@ namespace Avalonia.Controls
IsPressed = false;
e.Handled = true;
- if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this)))
+ var hittest = this.GetVisualsAt(e.GetPosition(this));
+
+ if (ClickMode == ClickMode.Release &&
+ hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this))
{
OnClick();
}
@@ -261,9 +266,9 @@ namespace Avalonia.Controls
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);
- if(property == CommandProperty)
+ if (property == CommandProperty)
{
- if(status?.ErrorType == BindingErrorType.Error)
+ if (status?.ErrorType == BindingErrorType.Error)
{
IsEnabled = false;
}
diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
index 4b58197ef3..5308a062ec 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
@@ -89,7 +89,7 @@ namespace Avalonia.Controls.Primitives
control.Clip = clip;
}
- clip.Rect = bounds.Clip.TransformToAABB(-bounds.Transform);
+ clip.Rect = bounds.Bounds;
}
private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
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.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
index d218960726..76f2898700 100644
--- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
@@ -1,7 +1,12 @@
using System;
using System.Windows.Input;
using Avalonia.Data;
-using Avalonia.Markup.Data;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -92,6 +97,205 @@ namespace Avalonia.Controls.UnitTests
Assert.False(target.IsEnabled);
}
+ [Fact]
+ public void Button_Raises_Click()
+ {
+ var mouse = Mock.Of();
+ var renderer = Mock.Of();
+ IInputElement captured = null;
+ Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50));
+ Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v);
+ Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+ Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns>((p, r, f) =>
+ r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
+
+ var target = new TestButton()
+ {
+ Bounds = new Rect(0, 0, 100, 100),
+ Renderer = renderer
+ };
+
+ bool clicked = false;
+
+ target.Click += (s, e) => clicked = true;
+
+ RaisePointerEnter(target, mouse);
+ RaisePointerMove(target, mouse);
+ RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+
+ Assert.Equal(captured, target);
+
+ RaisePointerReleased(target, mouse, MouseButton.Left);
+
+ Assert.Equal(captured, null);
+
+ Assert.True(clicked);
+ }
+
+ [Fact]
+ public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
+ {
+ var mouse = Mock.Of();
+ var renderer = Mock.Of();
+ IInputElement captured = null;
+ Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50));
+ Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v);
+ Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+ Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns>((p, r, f) =>
+ r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
+
+ var target = new TestButton()
+ {
+ Bounds = new Rect(0, 0, 100, 100),
+ Renderer = renderer
+ };
+
+ bool clicked = false;
+
+ target.Click += (s, e) => clicked = true;
+
+ RaisePointerEnter(target, mouse);
+ RaisePointerMove(target, mouse);
+ RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+ RaisePointerLeave(target, mouse);
+
+ Assert.Equal(captured, target);
+
+ RaisePointerReleased(target, mouse, MouseButton.Left);
+
+ Assert.Equal(captured, null);
+
+ Assert.False(clicked);
+ }
+
+ [Fact]
+ public void Button_With_RenderTransform_Raises_Click()
+ {
+ var mouse = Mock.Of();
+ var renderer = Mock.Of();
+ IInputElement captured = null;
+ Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50));
+ Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v);
+ Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+ Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns>((p, r, f) =>
+ r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
+ new IVisual[] { r } : new IVisual[0]);
+
+ var target = new TestButton()
+ {
+ Bounds = new Rect(0, 0, 100, 100),
+ RenderTransform = new TranslateTransform { X = 100, Y = 0 },
+ Renderer = renderer
+ };
+
+ //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels
+ //so mouse with x=150 coordinates should trigger click
+ //button shouldn't count on bounds to calculate pointer is in the over or not, but
+ //on avalonia event system, as renderer hit test will properly calculate whether to send
+ //mouse over events to button based on rendered bounds
+ //note: button also may have not rectangular shape and only renderer hit testing is reliable
+
+ bool clicked = false;
+
+ target.Click += (s, e) => clicked = true;
+
+ RaisePointerEnter(target, mouse);
+ RaisePointerMove(target, mouse);
+ RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+
+ Assert.Equal(captured, target);
+
+ RaisePointerReleased(target, mouse, MouseButton.Left);
+
+ Assert.Equal(captured, null);
+
+ Assert.True(clicked);
+ }
+
+ private class TestButton : Button, IRenderRoot
+ {
+ public TestButton()
+ {
+ IsVisible = true;
+ }
+
+ public new Rect Bounds
+ {
+ get => base.Bounds;
+ set => base.Bounds = value;
+ }
+
+ public Size ClientSize => throw new NotImplementedException();
+
+ public IRenderer Renderer { get; set; }
+
+ public double RenderScaling => throw new NotImplementedException();
+
+ public IRenderTarget CreateRenderTarget() => throw new NotImplementedException();
+
+ public void Invalidate(Rect rect) => throw new NotImplementedException();
+
+ public Point PointToClient(Point point) => throw new NotImplementedException();
+
+ public Point PointToScreen(Point point) => throw new NotImplementedException();
+ }
+
+ private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton)
+ {
+ button.RaiseEvent(new PointerPressedEventArgs
+ {
+ RoutedEvent = InputElement.PointerPressedEvent,
+ Source = button,
+ MouseButton = mouseButton,
+ ClickCount = clickCount,
+ Device = device,
+ });
+ }
+
+ private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton)
+ {
+ button.RaiseEvent(new PointerReleasedEventArgs
+ {
+ RoutedEvent = InputElement.PointerReleasedEvent,
+ Source = button,
+ MouseButton = mouseButton,
+ Device = device,
+ });
+ }
+
+ private void RaisePointerEnter(Button button, IMouseDevice device)
+ {
+ button.RaiseEvent(new PointerEventArgs
+ {
+ RoutedEvent = InputElement.PointerEnterEvent,
+ Source = button,
+ Device = device,
+ });
+ }
+
+ private void RaisePointerLeave(Button button, IMouseDevice device)
+ {
+ button.RaiseEvent(new PointerEventArgs
+ {
+ RoutedEvent = InputElement.PointerLeaveEvent,
+ Source = button,
+ Device = device,
+ });
+ }
+
+ private void RaisePointerMove(Button button, IMouseDevice device)
+ {
+ button.RaiseEvent(new PointerEventArgs
+ {
+ RoutedEvent = InputElement.PointerMovedEvent,
+ Source = button,
+ Device = device,
+ });
+ }
+
private class TestCommand : ICommand
{
private bool _enabled;
@@ -123,4 +327,4 @@ namespace Avalonia.Controls.UnitTests
}
}
}
-}
\ No newline at end of file
+}
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);
+ }
}
}