Browse Source

Updated adorners and hit testing to account for RenderTransforms (#538)

Updated adorners and hit testing to account for RenderTransforms. Fixes #433.
pull/549/head
Jeremy Koritzinsky 11 years ago
parent
commit
4fef640371
  1. 12
      Avalonia.sln
  2. 3
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  3. 13
      src/Avalonia.Input/InputExtensions.cs
  4. 4
      src/Avalonia.SceneGraph/Media/DrawingContext.cs
  5. 10
      src/Avalonia.SceneGraph/Media/Geometry.cs
  6. 7
      src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs
  7. 6
      src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
  8. 41
      src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs
  9. 24
      src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
  10. 5
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
  11. 5
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs
  12. 7
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  13. 3
      src/Skia/Avalonia.Skia/readme.md
  14. 7
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  15. 6
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  16. 316
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  17. 1
      tests/Avalonia.Input.UnitTests/packages.config
  18. 6
      tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj
  19. 51
      tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs

12
Avalonia.sln

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.24720.0 VisualStudioVersion = 14.0.25123.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject EndProject
@ -159,19 +159,25 @@ EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4
EndGlobalSection EndGlobalSection

3
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -58,6 +59,8 @@ namespace Avalonia.Controls.Primitives
if (info != null) if (info != null)
{ {
child.RenderTransform = new MatrixTransform(info.Bounds.Transform);
child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
child.Arrange(info.Bounds.Bounds); child.Arrange(info.Bounds.Bounds);
} }
else else

13
src/Avalonia.Input/InputExtensions.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.VisualTree;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -23,14 +24,13 @@ namespace Avalonia.Input
public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p) public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
{ {
Contract.Requires<ArgumentNullException>(element != null); Contract.Requires<ArgumentNullException>(element != null);
var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element);
var geometry = transformedBounds.GetTransformedBoundsGeometry();
if (element.Bounds.Contains(p) && if (element.IsVisible &&
element.IsVisible &&
element.IsHitTestVisible && element.IsHitTestVisible &&
element.IsEnabledCore) element.IsEnabledCore)
{ {
p -= element.Bounds.Position;
if (element.VisualChildren.Any()) if (element.VisualChildren.Any())
{ {
foreach (var child in ZSort(element.VisualChildren.OfType<IInputElement>())) foreach (var child in ZSort(element.VisualChildren.OfType<IInputElement>()))
@ -42,7 +42,10 @@ namespace Avalonia.Input
} }
} }
yield return element; if (geometry.FillContains(p))
{
yield return element;
}
} }
} }

4
src/Avalonia.SceneGraph/Media/DrawingContext.cs

@ -44,6 +44,8 @@ namespace Avalonia.Media
private Matrix _currentTransform = Matrix.Identity; private Matrix _currentTransform = Matrix.Identity;
private Matrix _currentContainerTransform = Matrix.Identity;
/// <summary> /// <summary>
/// Gets the current transform of the drawing context. /// Gets the current transform of the drawing context.
/// </summary> /// </summary>
@ -57,7 +59,7 @@ namespace Avalonia.Media
} }
} }
private Matrix _currentContainerTransform = Matrix.Identity; internal Matrix CurrentContainerTransform => _currentContainerTransform;
/// <summary> /// <summary>
/// Draws a bitmap image. /// Draws a bitmap image.

10
src/Avalonia.SceneGraph/Media/Geometry.cs

@ -66,5 +66,15 @@ namespace Avalonia.Media
{ {
return PlatformImpl.GetRenderBounds(strokeThickness); return PlatformImpl.GetRenderBounds(strokeThickness);
} }
/// <summary>
/// Indicates whether the geometry contains the specified point.
/// </summary>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
public bool FillContains(Point point)
{
return PlatformImpl.FillContains(point);
}
} }
} }

7
src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs

@ -24,5 +24,12 @@ namespace Avalonia.Platform
/// <param name="strokeThickness">The stroke thickness.</param> /// <param name="strokeThickness">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns> /// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(double strokeThickness); Rect GetRenderBounds(double strokeThickness);
/// <summary>
/// Indicates whether the geometry contains the specified point.
/// </summary>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
bool FillContains(Point point);
} }
} }

6
src/Avalonia.SceneGraph/Rendering/RendererMixin.cs

@ -122,6 +122,12 @@ namespace Avalonia.Rendering
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
visual.Render(context); visual.Render(context);
var transformed =
new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
if (visual is Visual)
{
BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
}
var lst = GetSortedVisualList(visual.VisualChildren); var lst = GetSortedVisualList(visual.VisualChildren);

41
src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Media;
namespace Avalonia.VisualTree namespace Avalonia.VisualTree
{ {
@ -16,6 +17,9 @@ namespace Avalonia.VisualTree
/// </remarks> /// </remarks>
public class BoundsTracker public class BoundsTracker
{ {
private static AttachedProperty<TransformedBounds> TransformedBoundsProperty =
AvaloniaProperty.RegisterAttached<BoundsTracker, Visual, TransformedBounds>("TransformedBounds");
/// <summary> /// <summary>
/// Starts tracking the specified visual. /// Starts tracking the specified visual.
/// </summary> /// </summary>
@ -23,42 +27,19 @@ namespace Avalonia.VisualTree
/// <returns>An observable that returns the tracked bounds.</returns> /// <returns>An observable that returns the tracked bounds.</returns>
public IObservable<TransformedBounds> Track(Visual visual) public IObservable<TransformedBounds> Track(Visual visual)
{ {
return Track(visual, (Visual)visual.GetVisualRoot()); return visual.GetObservable(TransformedBoundsProperty);
} }
/// <summary> internal static void SetTransformedBounds(Visual visual, TransformedBounds bounds)
/// Starts tracking the specified visual relative to another control.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="relativeTo">The control that the tracking should be relative to.</param>
/// <returns>An observable that returns the tracked bounds.</returns>
public IObservable<TransformedBounds> Track(Visual visual, Visual relativeTo)
{ {
var visuals = visual.GetSelfAndVisualAncestors() visual.SetValue(TransformedBoundsProperty, bounds);
.TakeWhile(x => x != relativeTo)
.Reverse();
var boundsSubscriptions = new List<IObservable<Rect>>();
foreach (var v in visuals.Cast<Visual>())
{
boundsSubscriptions.Add(v.GetObservable(Visual.BoundsProperty));
}
var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds);
// TODO: Track transform and clip rectangle.
return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity));
} }
/// <summary> /// <summary>
/// Sums a collection of rectangles. /// Gets the transformed bounds of the visual.
/// </summary> /// </summary>
/// <param name="rects">The collection of rectangles.</param> /// <param name="visual">The visual.</param>
/// <returns>The summed rectangle.</returns> /// <returns>The transformed bounds.</returns>
private static Rect ExtractBounds(IList<Rect> rects) public static TransformedBounds GetTransformedBounds(Visual visual) => visual.GetValue(TransformedBoundsProperty);
{
var position = rects.Select(x => x.Position).Aggregate((a, b) => a + b);
return new Rect(position, rects.Last().Size);
}
} }
} }

24
src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs

@ -1,15 +1,17 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
namespace Avalonia.VisualTree namespace Avalonia.VisualTree
{ {
/// <summary> /// <summary>
/// Holds information about the bounds of a control, together with a transform and a clip/ /// Holds information about the bounds of a control, together with a transform and a clip.
/// </summary> /// </summary>
public class TransformedBounds public struct TransformedBounds
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TransformedBounds"/> class. /// Initializes a new instance of the <see cref="TransformedBounds"/> struct.
/// </summary> /// </summary>
/// <param name="bounds">The control's bounds.</param> /// <param name="bounds">The control's bounds.</param>
/// <param name="clip">The control's clip rectangle.</param> /// <param name="clip">The control's clip rectangle.</param>
@ -35,5 +37,21 @@ namespace Avalonia.VisualTree
/// Gets the control's transform. /// Gets the control's transform.
/// </summary> /// </summary>
public Matrix Transform { get; } public Matrix Transform { get; }
public Geometry GetTransformedBoundsGeometry()
{
StreamGeometry geometry = new StreamGeometry();
using (var context = geometry.Open())
{
context.SetFillRule(FillRule.EvenOdd);
context.BeginFigure(Bounds.TopLeft * Transform, true);
context.LineTo(Bounds.TopRight * Transform);
context.LineTo(Bounds.BottomRight * Transform);
context.LineTo(Bounds.BottomLeft * Transform);
context.LineTo(Bounds.TopLeft * Transform);
context.EndFigure(true);
}
return geometry;
}
} }
} }

5
src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs

@ -62,6 +62,11 @@ namespace Avalonia.Cairo.Media
} }
} }
internal bool FillContains(Point point)
{
return _context.InFill(point.X, point.Y);
}
public void LineTo(Point point) public void LineTo(Point point)
{ {
if (this.Path == null) if (this.Path == null)

5
src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs

@ -67,5 +67,10 @@ namespace Avalonia.Cairo.Media
{ {
return _impl; return _impl;
} }
public bool FillContains(Point point)
{
return _impl.FillContains(point);
}
} }
} }

7
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@ -75,6 +75,13 @@ namespace Avalonia.Skia
return new StreamContext(this); return new StreamContext(this);
} }
public bool FillContains(Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
// return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point);
}
class StreamContext : IStreamGeometryContextImpl class StreamContext : IStreamGeometryContextImpl
{ {
private readonly StreamGeometryImpl _geometryImpl; private readonly StreamGeometryImpl _geometryImpl;

3
src/Skia/Avalonia.Skia/readme.md

@ -4,6 +4,9 @@ BitmapImpl
- constructor from Width/Height - constructor from Width/Height
- Save - Save
StreamGeometryImpl
- Hit testing in Geometry missing as SkiaSharp does not expose this
DrawingContextImpl DrawingContextImpl
- Alpha support missing as SkiaSharp does not expose this - Alpha support missing as SkiaSharp does not expose this
- Gradient Shader caching? - Gradient Shader caching?

7
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -84,5 +84,12 @@ namespace Avalonia.Direct2D1.Media
return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
} }
} }
public bool FillContains(Point point)
{
return Geometry.FillContainsPoint(point.ToSharpDX());
}
} }
} }

6
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" /> <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@ -35,6 +35,10 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Moq, Version=4.2.1510.2205, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />

316
tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs

@ -1,9 +1,17 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
using System.Collections.Generic;
using System.IO;
namespace Avalonia.Input.UnitTests namespace Avalonia.Input.UnitTests
{ {
@ -12,59 +20,73 @@ namespace Avalonia.Input.UnitTests
[Fact] [Fact]
public void InputHitTest_Should_Find_Control_At_Point() public void InputHitTest_Should_Find_Control_At_Point()
{ {
var container = new Decorator using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{ {
Width = 200, var container = new Decorator
Height = 200,
Child = new Border
{ {
Width = 100, Width = 200,
Height = 100, Height = 200,
HorizontalAlignment = HorizontalAlignment.Center, Child = new Border
VerticalAlignment = VerticalAlignment.Center {
} Width = 100,
}; Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
container.Measure(Size.Infinity); container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize)); container.Arrange(new Rect(container.DesiredSize));
var result = container.InputHitTest(new Point(100, 100)); var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
context.Render(container);
Assert.Equal(container.Child, result); var result = container.InputHitTest(new Point(100, 100));
Assert.Equal(container.Child, result);
}
} }
[Fact] [Fact]
public void InputHitTest_Should_Not_Find_Control_Outside_Point() public void InputHitTest_Should_Not_Find_Control_Outside_Point()
{ {
var container = new Decorator using (UnitTestApplication.Start(new TestServices(renderInterface:new MockRenderInterface())))
{ {
Width = 200, var container = new Decorator
Height = 200,
Child = new Border
{ {
Width = 100, Width = 200,
Height = 100, Height = 200,
HorizontalAlignment = HorizontalAlignment.Center, Child = new Border
VerticalAlignment = VerticalAlignment.Center {
} Width = 100,
}; Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize));
container.Measure(Size.Infinity); var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
container.Arrange(new Rect(container.DesiredSize)); context.Render(container);
var result = container.InputHitTest(new Point(10, 10)); var result = container.InputHitTest(new Point(10, 10));
Assert.Equal(container, result); Assert.Equal(container, result);
}
} }
[Fact] [Fact]
public void InputHitTest_Should_Find_Top_Control_At_Point() public void InputHitTest_Should_Find_Top_Control_At_Point()
{ {
var container = new Panel using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{ {
Width = 200, var container = new Panel
Height = 200, {
Children = new Controls.Controls Width = 200,
Height = 200,
Children = new Controls.Controls
{ {
new Border new Border
{ {
@ -81,24 +103,30 @@ namespace Avalonia.Input.UnitTests
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
} }
} }
}; };
container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize));
container.Measure(Size.Infinity); var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
container.Arrange(new Rect(container.DesiredSize)); context.Render(container);
var result = container.InputHitTest(new Point(100, 100)); var result = container.InputHitTest(new Point(100, 100));
Assert.Equal(container.Children[1], result); Assert.Equal(container.Children[1], result);
}
} }
[Fact] [Fact]
public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder()
{ {
var container = new Panel using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{ {
Width = 200, var container = new Panel
Height = 200, {
Children = new Controls.Controls Width = 200,
Height = 200,
Children = new Controls.Controls
{ {
new Border new Border
{ {
@ -116,14 +144,210 @@ namespace Avalonia.Input.UnitTests
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
} }
} }
}; };
container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize));
var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
context.Render(container);
var result = container.InputHitTest(new Point(100, 100));
Assert.Equal(container.Children[0], result);
}
}
[Fact]
public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
Border target;
var container = new Panel
{
Width = 200,
Height = 200,
Children = new Controls.Controls
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = target = new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform(110, 110),
}
},
}
};
container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize));
var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
context.Render(container);
var result = container.InputHitTest(new Point(120, 120));
Assert.Equal(target, result);
}
}
class MockRenderInterface : IPlatformRenderInterface
{
public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
{
throw new NotImplementedException();
}
public IRenderTarget CreateRenderer(IPlatformHandle handle)
{
throw new NotImplementedException();
}
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
{
throw new NotImplementedException();
}
public IStreamGeometryImpl CreateStreamGeometry()
{
return new MockStreamGeometry();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(string fileName)
{
throw new NotImplementedException();
}
class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
public Rect Bounds
{
get
{
throw new NotImplementedException();
}
}
public Matrix Transform
{
get
{
throw new NotImplementedException();
}
container.Measure(Size.Infinity); set
container.Arrange(new Rect(container.DesiredSize)); {
throw new NotImplementedException();
}
}
var result = container.InputHitTest(new Point(100, 100)); public IStreamGeometryImpl Clone()
{
return this;
}
Assert.Equal(container.Children[0], result); public bool FillContains(Point point)
{
return _impl.FillContains(point);
}
public Rect GetRenderBounds(double strokeThickness)
{
throw new NotImplementedException();
}
public IStreamGeometryContextImpl Open()
{
return _impl;
}
class MockStreamGeometryContext : IStreamGeometryContextImpl
{
private List<Point> points = new List<Point>();
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
throw new NotImplementedException();
}
public void BeginFigure(Point startPoint, bool isFilled)
{
points.Add(startPoint);
}
public void CubicBezierTo(Point point1, Point point2, Point point3)
{
throw new NotImplementedException();
}
public void Dispose()
{
}
public void EndFigure(bool isClosed)
{
}
public void LineTo(Point point)
{
points.Add(point);
}
public void QuadraticBezierTo(Point control, Point endPoint)
{
throw new NotImplementedException();
}
public void SetFillRule(FillRule fillRule)
{
}
public bool FillContains(Point point)
{
// Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html
// to determine if the point is in the geometry (since it will always be convex in this situation)
for (int i = 0; i < points.Count; i++)
{
var a = points[i];
var b = points[(i + 1) % points.Count];
var c = points[(i + 2) % points.Count];
Vector v0 = c - a;
Vector v1 = b - a;
Vector v2 = point - a;
var dot00 = v0 * v0;
var dot01 = v0 * v1;
var dot02 = v0 * v2;
var dot11 = v1 * v1;
var dot12 = v1 * v2;
var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
}
return false;
}
}
}
} }
} }
} }

1
tests/Avalonia.Input.UnitTests/packages.config

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Moq" version="4.2.1510.2205" targetFramework="net45" />
<package id="xunit" version="2.1.0" targetFramework="net45" /> <package id="xunit" version="2.1.0" targetFramework="net45" />
<package id="xunit.abstractions" version="2.0.0" targetFramework="net45" /> <package id="xunit.abstractions" version="2.0.0" targetFramework="net45" />
<package id="xunit.assert" version="2.1.0" targetFramework="net45" /> <package id="xunit.assert" version="2.1.0" targetFramework="net45" />

6
tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" /> <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
<PropertyGroup> <PropertyGroup>
@ -127,6 +127,10 @@
<Project>{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}</Project> <Project>{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}</Project>
<Name>Avalonia.Styling</Name> <Name>Avalonia.Styling</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj">
<Project>{88060192-33D5-4932-B0F9-8BD2763E857D}</Project>
<Name>Avalonia.UnitTests</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" /> <None Include="app.config" />

51
tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs

@ -8,7 +8,11 @@ using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Rendering;
using Xunit; using Xunit;
using Avalonia.Media;
using Moq;
using Avalonia.UnitTests;
namespace Avalonia.SceneGraph.UnitTests.VisualTree namespace Avalonia.SceneGraph.UnitTests.VisualTree
{ {
@ -17,36 +21,37 @@ namespace Avalonia.SceneGraph.UnitTests.VisualTree
[Fact] [Fact]
public void Should_Track_Bounds() public void Should_Track_Bounds()
{ {
var target = new BoundsTracker(); using (UnitTestApplication.Start(TestServices.StyledWindow))
var control = default(Rectangle);
var tree = new Decorator
{ {
Padding = new Thickness(10), var target = new BoundsTracker();
Child = new Decorator var control = default(Rectangle);
var tree = new Decorator
{ {
Padding = new Thickness(5), Padding = new Thickness(10),
Child = control = new Rectangle Child = new Decorator
{ {
Width = 15, Padding = new Thickness(5),
Height = 15, Child = control = new Rectangle
}, {
} Width = 15,
}; Height = 15,
},
}
};
tree.Measure(Size.Infinity); var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
tree.Arrange(new Rect(0, 0, 100, 100));
var track = target.Track(control, tree); tree.Measure(Size.Infinity);
var results = new List<TransformedBounds>(); tree.Arrange(new Rect(0, 0, 100, 100));
track.Subscribe(results.Add); context.Render(tree);
Assert.Equal(new Rect(42, 42, 15, 15), results[0].Bounds); var track = target.Track(control);
var results = new List<TransformedBounds>();
track.Subscribe(results.Add);
tree.Padding = new Thickness(15); Assert.Equal(new Rect(0, 0, 15, 15), results[0].Bounds);
tree.Measure(Size.Infinity); Assert.Equal(Matrix.CreateTranslation(42, 42), results[0].Transform);
tree.Arrange(new Rect(0, 0, 100, 100)); }
Assert.Equal(new Rect(37, 37, 15, 15), results[1].Bounds);
} }
} }
} }

Loading…
Cancel
Save