Browse Source

Initial implementation of scenegraph hit testing.

Based solely on control bounds as before.
scenegraph-after-breakage
Steven Kirk 9 years ago
parent
commit
d91d1829ac
  1. 7
      samples/RenderTest/RenderTest.v2.ncrunchproject
  2. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  3. 12
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  4. 2
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  5. 6
      src/Avalonia.Visuals/Rendering/Renderer.cs
  6. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  7. 3
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs
  8. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  9. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  10. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  11. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  12. 48
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  13. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  14. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  15. 9
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  16. 13
      src/Avalonia.Visuals/Rendering/ZIndexComparer.cs
  17. 28
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  18. 1
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  19. 484
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  20. 14
      tests/Avalonia.LeakTests/ControlTests.cs
  21. 8
      tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject
  22. 10
      tests/Avalonia.UnitTests/TestRoot.cs
  23. 11
      tests/Avalonia.UnitTests/TestServices.cs
  24. 17
      tests/Avalonia.UnitTests/UnitTestApplication.cs
  25. 7
      tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject
  26. 66
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  27. 250
      tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

7
samples/RenderTest/RenderTest.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>MissingOrIgnoredProjectReference</HiddenWarnings>
</ProjectConfiguration>

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -124,6 +124,7 @@
<Compile Include="Rendering\SceneGraph\SceneBuilder.cs" />
<Compile Include="Rendering\SceneGraph\TextNode.cs" />
<Compile Include="Rendering\SceneGraph\VisualNode.cs" />
<Compile Include="Rendering\ZIndexComparer.cs" />
<Compile Include="RenderTargetCorruptedException.cs" />
<Compile Include="Size.cs" />
<Compile Include="Thickness.cs" />

12
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -6,6 +6,7 @@ using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System.Collections.Generic;
namespace Avalonia.Rendering
{
@ -30,6 +31,7 @@ namespace Avalonia.Rendering
_root = root;
_scene = new Scene(root);
_needsUpdate = true;
_renderLoop = renderLoop;
_renderLoop.Tick += OnRenderLoopTick;
}
@ -50,6 +52,16 @@ namespace Avalonia.Rendering
_renderLoop.Tick -= OnRenderLoopTick;
}
public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
{
if (_needsUpdate)
{
UpdateScene();
}
return _scene.HitTest(p, filter);
}
public void Render(Rect rect)
{
if (_renderTarget == null)

2
src/Avalonia.Visuals/Rendering/IRenderer.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.VisualTree;
using System.Collections.Generic;
namespace Avalonia.Rendering
{
@ -11,6 +12,7 @@ namespace Avalonia.Rendering
bool DrawFps { get; set; }
void AddDirty(IVisual visual);
IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter);
void Render(Rect rect);
}
}

6
src/Avalonia.Visuals/Rendering/Renderer.cs

@ -4,6 +4,7 @@
using System;
using Avalonia.Platform;
using Avalonia.VisualTree;
using System.Collections.Generic;
namespace Avalonia.Rendering
{
@ -35,6 +36,11 @@ namespace Avalonia.Rendering
_renderLoop.Tick -= OnRenderLoopTick;
}
public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
{
throw new NotImplementedException();
}
public void Render(Rect rect)
{
if (_renderTarget == null)

5
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -35,5 +35,10 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry);
}
public bool HitTest(Point p)
{
throw new NotImplementedException();
}
}
}

3
src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs

@ -2,13 +2,14 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using Avalonia.Media;
namespace Avalonia.Rendering.SceneGraph
{
public interface ISceneNode
{
bool HitTest(Point p);
void Render(IDrawingContextImpl context);
}
}

4
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -9,7 +9,9 @@ namespace Avalonia.Rendering.SceneGraph
{
public interface IVisualNode : ISceneNode
{
IReadOnlyList<ISceneNode> Children { get; }
IVisual Visual { get; }
Rect ClipBounds { get; set; }
bool ClipToBounds { get; set; }
IReadOnlyList<ISceneNode> Children { get; }
}
}

5
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@ -38,5 +38,10 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = Transform;
context.DrawImage(Source, Opacity, SourceRect, DestRect);
}
public bool HitTest(Point p)
{
throw new NotImplementedException();
}
}
}

5
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@ -31,5 +31,10 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = Transform;
context.DrawLine(Pen, P1, P2);
}
public bool HitTest(Point p)
{
throw new NotImplementedException();
}
}
}

5
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -46,5 +46,10 @@ namespace Avalonia.Rendering.SceneGraph
context.DrawRectangle(Pen, Rect, CornerRadius);
}
}
public bool HitTest(Point p)
{
throw new NotImplementedException();
}
}
}

48
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -31,6 +31,14 @@ namespace Avalonia.Rendering.SceneGraph
_index.Add(node.Visual, node);
}
public Scene Clone()
{
var index = new Dictionary<IVisual, IVisualNode>();
var root = (VisualNode)Clone((VisualNode)Root, null, index);
var result = new Scene(root, index);
return result;
}
public IVisualNode FindNode(IVisual visual)
{
IVisualNode node;
@ -38,12 +46,9 @@ namespace Avalonia.Rendering.SceneGraph
return node;
}
public Scene Clone()
public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
{
var index = new Dictionary<IVisual, IVisualNode>();
var root = (VisualNode)Clone((VisualNode)Root, null, index);
var result = new Scene(root, index);
return result;
return HitTest(Root, p, null, filter);
}
private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary<IVisual, IVisualNode> index)
@ -68,5 +73,38 @@ namespace Avalonia.Rendering.SceneGraph
return result;
}
private IEnumerable<IVisual> HitTest(IVisualNode node, Point p, Rect? clip, Func<IVisual, bool> filter)
{
if (filter?.Invoke(node.Visual) != false)
{
if (node.ClipToBounds)
{
// TODO: Handle geometry clip.
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
}
if (!clip.HasValue || clip.Value.Contains(p))
{
for (var i = node.Children.Count - 1; i >= 0; --i)
{
var visualChild = node.Children[i] as IVisualNode;
if (visualChild != null)
{
foreach (var h in HitTest(visualChild, p, clip, filter))
{
yield return h;
}
}
}
if (node.HitTest(p))
{
yield return node.Visual;
}
}
}
}
}
}

14
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -55,7 +56,7 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushTransformContainer())
{
node.Transform = contextImpl.Transform;
node.Bounds = bounds;
node.ClipBounds = bounds * node.Transform;
node.ClipToBounds = clipToBounds;
node.GeometryClip = visual.Clip;
node.Opacity = opacity;
@ -63,16 +64,7 @@ namespace Avalonia.Rendering.SceneGraph
visual.Render(context);
#pragma warning disable 0618
var transformed = new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
#pragma warning restore 0618
if (visual is Visual)
{
BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
}
foreach (var child in visual.VisualChildren)
foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
{
Update(context, scene, child, node);
}

5
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@ -35,5 +35,10 @@ namespace Avalonia.Rendering.SceneGraph
origin == Origin &&
Equals(text, Text);
}
public bool HitTest(Point p)
{
throw new NotImplementedException();
}
}
}

9
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph
public IVisual Visual { get; }
public Matrix Transform { get; set; }
public Rect Bounds { get; set; }
public Rect ClipBounds { get; set; }
public bool ClipToBounds { get; set; }
public Geometry GeometryClip { get; set; }
public double Opacity { get; set; }
@ -32,6 +32,11 @@ namespace Avalonia.Rendering.SceneGraph
return new VisualNode(Visual);
}
public bool HitTest(Point p)
{
return ClipBounds.Contains(p);
}
public void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
@ -43,7 +48,7 @@ namespace Avalonia.Rendering.SceneGraph
if (ClipToBounds)
{
context.PushClip(Bounds);
context.PushClip(ClipBounds);
}
foreach (var child in Children)

13
src/Avalonia.Visuals/Rendering/ZIndexComparer.cs

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class ZIndexComparer : IComparer<IVisual>
{
public static readonly ZIndexComparer Instance = new ZIndexComparer();
public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
}
}

28
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -1,6 +1,7 @@
// 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.
using Avalonia.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
@ -102,26 +103,9 @@ namespace Avalonia.VisualTree
{
Contract.Requires<ArgumentNullException>(visual != null);
if (filter?.Invoke(visual) != false)
{
bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)visual)?.Contains(p) == true;
if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Any())
{
foreach (var child in visual.VisualChildren.SortByZIndex())
{
foreach (var result in child.GetVisualsAt(p, filter))
{
yield return result;
}
}
}
if (containsPoint)
{
yield return visual;
}
}
var root = visual.GetVisualRoot();
p = visual.TranslatePoint(p, root);
return root.Renderer.HitTest(p, filter);
}
/// <summary>
@ -197,11 +181,11 @@ namespace Avalonia.VisualTree
/// <returns>
/// The root visual or null if the visual is not rooted.
/// </returns>
public static IVisual GetVisualRoot(this IVisual visual)
public static IRenderRoot GetVisualRoot(this IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.VisualRoot as IVisual;
return visual as IRenderRoot ?? visual.VisualRoot;
}
/// <summary>

1
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -86,7 +86,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="InputElement_Focus.cs" />
<Compile Include="InputElement_HitTesting.cs" />
<Compile Include="KeyboardNavigationTests_Arrows.cs" />
<Compile Include="KeyboardNavigationTests_Tab.cs" />
<Compile Include="KeyGestureParseTests.cs" />

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

@ -1,484 +0,0 @@
// 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.
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class InputElement_HitTesting
{
[Fact]
public void InputHitTest_Should_Find_Control_At_Point()
{
using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
var container = new Decorator
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.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.Child, result);
}
}
[Fact]
public void InputHitTest_Should_Not_Find_Control_Outside_Point()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
var container = new Decorator
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.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(10, 10));
Assert.Equal(container, result);
}
}
[Fact]
public void InputHitTest_Should_Find_Top_Control_At_Point()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
var container = new Panel
{
Width = 200,
Height = 200,
Children = new Controls.Controls
{
new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.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[1], result);
}
}
[Fact]
public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
var container = new Panel
{
Width = 200,
Height = 200,
Children = new Controls.Controls
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.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,
ClipToBounds = false,
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);
}
}
[Fact]
public void InputHitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
Border target;
var container = new Panel
{
Width = 100,
Height = 200,
Children = new Controls.Controls
{
new Panel()
{
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children = new Controls.Controls
{
(target = new Border()
{
Width = 100,
Height = 100,
Margin = new Thickness(0, -100, 0, 0)
})
}
}
}
};
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(50, 50));
Assert.NotEqual(target, result);
Assert.Equal(container, result);
}
}
[Fact]
public void InputHitTest_Should_Not_Find_Control_Outside_Scroll_ViewPort()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
{
Border target;
Border item1;
Border item2;
ScrollContentPresenter scroll;
var container = new Panel
{
Width = 100,
Height = 200,
Children = new Controls.Controls
{
(target = new Border()
{
Width = 100,
Height = 100
}),
new Border()
{
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
Child = scroll = new ScrollContentPresenter()
{
Content = new StackPanel()
{
Children = new Controls.Controls
{
(item1 = new Border()
{
Width = 100,
Height = 100,
}),
(item2 = new Border()
{
Width = 100,
Height = 100,
}),
}
}
}
}
}
};
scroll.UpdateChild();
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(50, 150));
Assert.Equal(item1, result);
result = container.InputHitTest(new Point(50, 50));
Assert.Equal(target, result);
scroll.Offset = new Vector(0, 100);
//we don't have setup LayoutManager so we will make it manually
scroll.Parent.InvalidateArrange();
container.InvalidateArrange();
container.Arrange(new Rect(container.DesiredSize));
context.Render(container);
result = container.InputHitTest(new Point(50, 150));
Assert.Equal(item2, result);
result = container.InputHitTest(new Point(50, 50));
Assert.NotEqual(item1, result);
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 CreateRenderTarget(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();
}
set
{
throw new NotImplementedException();
}
}
public IStreamGeometryImpl Clone()
{
return this;
}
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;
}
}
}
}
}
}

14
tests/Avalonia.LeakTests/ControlTests.cs

@ -54,7 +54,6 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
@ -90,7 +89,6 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
@ -127,7 +125,6 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
@ -163,7 +160,6 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
@ -207,7 +203,6 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
@ -294,21 +289,12 @@ namespace Avalonia.LeakTests
};
var result = run();
PurgeMoqReferences();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TreeView>()).ObjectsCount));
}
}
private static void PurgeMoqReferences()
{
// Moq holds onto references in its mock of IRenderer in case we want to check if a method has been called;
// clear these.
var renderer = Mock.Get(AvaloniaLocator.Current.GetService<IRenderer>());
renderer.ResetCalls();
}
private class Node
{
public string Name { get; set; }

8
tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject

@ -7,7 +7,7 @@
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
@ -17,9 +17,9 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>

10
tests/Avalonia.UnitTests/TestRoot.cs

@ -15,6 +15,13 @@ namespace Avalonia.UnitTests
{
private readonly NameScope _nameScope = new NameScope();
public TestRoot()
{
var rendererFactory = AvaloniaLocator.Current.GetService<IRendererFactory>();
var renderLoop = AvaloniaLocator.Current.GetService<IRenderLoop>();
Renderer = rendererFactory?.CreateRenderer(this, renderLoop);
}
event EventHandler<NameScopeEventArgs> INameScope.Registered
{
add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; }
@ -41,7 +48,7 @@ namespace Avalonia.UnitTests
public IRenderTarget RenderTarget => null;
public IRenderer Renderer => null;
public IRenderer Renderer { get; }
public IRenderTarget CreateRenderTarget()
{
@ -50,7 +57,6 @@ namespace Avalonia.UnitTests
public void Invalidate(Rect rect)
{
throw new NotImplementedException();
}
public Point PointToClient(Point p) => p;

11
tests/Avalonia.UnitTests/TestServices.cs

@ -21,7 +21,7 @@ namespace Avalonia.UnitTests
assetLoader: new AssetLoader(),
layoutManager: new LayoutManager(),
platform: new AppBuilder().RuntimePlatform,
renderer: Mock.Of<IRenderer>(),
renderer: (_, __) => Mock.Of<IRenderer>(),
renderInterface: CreateRenderInterfaceMock(),
renderLoop: Mock.Of<IRenderLoop>(),
standardCursorFactory: Mock.Of<IStandardCursorFactory>(),
@ -42,6 +42,9 @@ namespace Avalonia.UnitTests
public static readonly TestServices MockThreadingInterface = new TestServices(
threadingInterface: Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true));
public static readonly TestServices RealDeferredRenderer = new TestServices(
renderer: (root, loop) => new DeferredRenderer(root, loop));
public static readonly TestServices RealFocus = new TestServices(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
@ -60,7 +63,7 @@ namespace Avalonia.UnitTests
Func<IKeyboardDevice> keyboardDevice = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
IRenderer renderer = null,
Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IStandardCursorFactory standardCursorFactory = null,
@ -93,7 +96,7 @@ namespace Avalonia.UnitTests
public Func<IKeyboardDevice> KeyboardDevice { get; }
public ILayoutManager LayoutManager { get; }
public IRuntimePlatform Platform { get; }
public IRenderer Renderer { get; }
public Func<IRenderRoot, IRenderLoop, IRenderer> Renderer { get; }
public IPlatformRenderInterface RenderInterface { get; }
public IRenderLoop RenderLoop { get; }
public IStandardCursorFactory StandardCursorFactory { get; }
@ -110,7 +113,7 @@ namespace Avalonia.UnitTests
Func<IKeyboardDevice> keyboardDevice = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
IRenderer renderer = null,
Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IStandardCursorFactory standardCursorFactory = null,

17
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -43,7 +43,7 @@ namespace Avalonia.UnitTests
.Bind<IKeyboardDevice>().ToConstant(Services.KeyboardDevice?.Invoke())
.Bind<ILayoutManager>().ToConstant(Services.LayoutManager)
.Bind<IRuntimePlatform>().ToConstant(Services.Platform)
.Bind<IRenderer>().ToConstant(Services.Renderer)
.Bind<IRendererFactory>().ToConstant(new RendererFactory(Services.Renderer))
.Bind<IPlatformRenderInterface>().ToConstant(Services.RenderInterface)
.Bind<IRenderLoop>().ToConstant(Services.RenderLoop)
.Bind<IPlatformThreadingInterface>().ToConstant(Services.ThreadingInterface)
@ -58,5 +58,20 @@ namespace Avalonia.UnitTests
Styles.AddRange(styles);
}
}
private class RendererFactory : IRendererFactory
{
Func<IRenderRoot, IRenderLoop, IRenderer> _func;
public RendererFactory(Func<IRenderRoot, IRenderLoop, IRenderer> func)
{
_func = func;
}
public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
{
return _func?.Invoke(root, renderLoop);
}
}
}
}

7
tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>AbnormalReferenceResolution;LongTestTimesWithoutParallelExecution</HiddenWarnings>
</ProjectConfiguration>

66
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -1,8 +1,10 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
@ -52,6 +54,70 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
}
}
[Fact]
public void Should_Respect_ZIndex()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border front;
Border back;
var tree = new TestRoot
{
Child = new Panel
{
Children =
{
(front = new Border
{
ZIndex = 1,
}),
(back = new Border
{
ZIndex = 0,
}),
}
}
};
var result = SceneBuilder.Update(new Scene(tree));
var panelNode = result.FindNode(tree.Child);
var expected = new IVisual[] { back, front };
var actual = panelNode.Children.OfType<IVisualNode>().Select(x => x.Visual).ToArray();
Assert.Equal(expected, actual);
}
}
[Fact]
public void ClipBounds_Should_Be_In_Global_Coordinates()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border target;
var tree = new TestRoot
{
Child = new Decorator
{
Margin = new Thickness(24, 26),
Child = target = new Border
{
Margin = new Thickness(26, 24),
Width = 100,
Height = 100,
}
}
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var result = SceneBuilder.Update(new Scene(tree));
var targetNode = result.FindNode(target);
Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds);
}
}
[Fact]
public void Should_Update_Border_Background_Node()
{

250
tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

@ -11,6 +11,7 @@ using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
using System;
namespace Avalonia.Visuals.UnitTests.VisualTree
{
@ -19,9 +20,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Find_Controls_At_Point()
{
using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
var container = new Decorator
var container = new TestRoot
{
Width = 200,
Height = 200,
@ -49,9 +50,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point()
{
using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
var container = new Decorator
var container = new TestRoot
{
Width = 200,
Height = 200,
@ -85,9 +86,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Not_Find_Control_Outside_Point()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
var container = new Decorator
var container = new TestRoot
{
Width = 200,
Height = 200,
@ -115,27 +116,31 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Return_Top_Controls_First()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
var container = new Panel
Panel container;
var root = new TestRoot
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Child = container = new Panel
{
new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
Width = 200,
Height = 200,
Children = new Controls.Controls
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
}
};
@ -155,36 +160,40 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
var container = new Panel
Panel container;
var root = new TestRoot
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Child = container = new Panel
{
new Border
Width = 200,
Height = 200,
Children = new Controls.Controls
{
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 75,
Height = 75,
ZIndex = 2,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 75,
Height = 75,
ZIndex = 2,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
}
};
@ -204,32 +213,36 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
Border target;
var container = new Panel
Panel container;
var root = new TestRoot
{
Width = 200,
Height = 200,
ClipToBounds = false,
Children = new Controls.Controls
Child = container = new Panel
{
new Border
Width = 200,
Height = 200,
ClipToBounds = false,
Children = new Controls.Controls
{
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = target = new Border
new Border
{
Width = 50,
Height = 50,
Width = 100,
Height = 100,
ZIndex = 1,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform(110, 110),
}
},
Child = target = new Border
{
Width = 50,
Height = 50,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform(110, 110),
}
},
}
}
};
@ -248,30 +261,33 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
Border target;
var container = new Panel
Panel container;
var root = new TestRoot
{
Width = 100,
Height = 200,
Children = new Controls.Controls
Child = container = new Panel
{
new Panel()
Width = 100,
Height = 200,
Children = new Controls.Controls
{
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children = new Controls.Controls
new Panel()
{
(target = new Border()
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children = new Controls.Controls
{
Width = 100,
Height = 100,
Margin = new Thickness(0, -100, 0, 0)
})
(target = new Border()
{
Width = 100,
Height = 100,
Margin = new Thickness(0, -100, 0, 0)
})
}
}
}
}
@ -292,45 +308,48 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
[Fact]
public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
using (TestApplication())
{
Border target;
Border item1;
Border item2;
ScrollContentPresenter scroll;
var container = new Panel
Panel container;
var root = new TestRoot
{
Width = 100,
Height = 200,
Children = new Controls.Controls
Child = container = new Panel
{
(target = new Border()
{
Width = 100,
Height = 100
}),
new Border()
Width = 100,
Height = 200,
Children = new Controls.Controls
{
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
Child = scroll = new ScrollContentPresenter()
(target = new Border()
{
Content = new StackPanel()
Width = 100,
Height = 100
}),
new Border()
{
Width = 100,
Height = 100,
Margin = new Thickness(0, 100, 0, 0),
Child = scroll = new ScrollContentPresenter()
{
Children = new Controls.Controls
Content = new StackPanel()
{
(item1 = new Border()
{
Width = 100,
Height = 100,
}),
(item2 = new Border()
Children = new Controls.Controls
{
Width = 100,
Height = 100,
}),
(item1 = new Border()
{
Width = 100,
Height = 100,
}),
(item2 = new Border()
{
Width = 100,
Height = 100,
}),
}
}
}
}
@ -373,5 +392,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
Assert.Equal(target, result);
}
}
private IDisposable TestApplication()
{
return UnitTestApplication.Start(
new TestServices(
renderInterface: new MockRenderInterface(),
renderLoop: Mock.Of<IRenderLoop>(),
renderer: (root, loop) => new DeferredRenderer(root, loop)));
}
}
}

Loading…
Cancel
Save