Browse Source

Merge branch 'master' into issues/2058

pull/2059/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
2ea43529da
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      parameters.cake
  2. 2
      samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
  3. 7
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  4. 11
      src/Avalonia.Controls/Button.cs
  5. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  6. 16
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  7. 23
      src/Avalonia.Visuals/Visual.cs
  8. 29
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
  9. 208
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  10. 98
      tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs
  11. 44
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs

2
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);

2
samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml

@ -19,7 +19,7 @@
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="{Binding #rotation.Value}"/>
</LayoutTransformControl.LayoutTransform>
<TextBlock>Layout Transform</TextBlock>
<Button Background="White">Layout Transform</Button>
</LayoutTransformControl>
</Grid>
</DockPanel>

7
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:

11
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;
}

2
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)

16
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<IVisual> HitTest(
private static IEnumerable<IVisual> HitTest(
IVisual visual,
Point p,
Func<IVisual, bool> filter)

23
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
/// <param name="ancestor">The ancestor visual.</param>
/// <param name="visual">The visual.</param>
/// <returns>The visual offset.</returns>
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)

29
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<int>(new[] { 0, 1, 2, 3 });
var source = new AvaloniaList<int>(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();

208
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<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(50, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((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<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(200, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((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<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(150, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((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
}
}
}
}
}

98
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<Visual>();
var child = new Mock<Visual>() { CallBase = true };
var renderRoot = visual.As<IRenderRoot>();
visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
child.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(10, 10, 100, 100));
child.As<IVisual>().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<Visual>();
var child = new Mock<Visual>() { CallBase = true };
var renderRoot = visual.As<IRenderRoot>();
visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
child.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(100, 100, 100, 100));
child.As<IVisual>().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<Visual>() { CallBase = true };
var child = new Mock<Visual>() { CallBase = true };
var renderRoot = visual.As<IRenderRoot>();
var renderTarget = visual.As<IRenderTarget>();
renderRoot.Setup(r => r.CreateRenderTarget()).Returns(renderTarget.Object);
renderTarget.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(Mock.Of<IDrawingContextImpl>());
visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
visual.As<IVisual>().Setup(v => v.VisualChildren).Returns(new AvaloniaList<IVisual>() { child.As<IVisual>().Object });
Rect childBounds = new Rect(0, 0, 100, 100);
child.As<IVisual>().Setup(v => v.Bounds).Returns(() => childBounds);
child.As<IVisual>().Setup(v => v.VisualParent).Returns(visual.Object);
child.As<IVisual>().Setup(v => v.VisualChildren).Returns(new AvaloniaList<IVisual>());
var invalidationCalls = new List<Rect>();
renderRoot.Setup(v => v.Invalidate(It.IsAny<Rect>())).Callback<Rect>(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]);
}
}
}

44
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<InvalidOperationException>(() => 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);
}
}
}

Loading…
Cancel
Save