diff --git a/src/Perspex.Input/IInputElement.cs b/src/Perspex.Input/IInputElement.cs index d6e5378f02..e8658c99b0 100644 --- a/src/Perspex.Input/IInputElement.cs +++ b/src/Perspex.Input/IInputElement.cs @@ -114,14 +114,8 @@ namespace Perspex.Input void Focus(); /// - /// Returns the input element that can be found within the current control at the specified - /// position. + /// Gets the key bindings for the element. /// - /// The position, in control coordinates. - /// The at the specified position. - IInputElement InputHitTest(Point p); - - List KeyBindings { get; } } } diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 43cfc840c6..7e0907c9e8 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -339,17 +339,6 @@ namespace Perspex.Input public List KeyBindings { get; } = new List(); - /// - /// Returns the input element that can be found within the current control at the specified - /// position. - /// - /// The position, in control coordinates. - /// The at the specified position. - public IInputElement InputHitTest(Point p) - { - return this.GetInputElementsAt(p).FirstOrDefault(); - } - /// /// Focuses the control. /// diff --git a/src/Perspex.Input/InputExtensions.cs b/src/Perspex.Input/InputExtensions.cs index ef3f848073..50d826b2a9 100644 --- a/src/Perspex.Input/InputExtensions.cs +++ b/src/Perspex.Input/InputExtensions.cs @@ -7,8 +7,19 @@ using System.Linq; namespace Perspex.Input { + /// + /// Defines extensions for the interface. + /// public static class InputExtensions { + /// + /// Returns the active input elements at a point on an . + /// + /// The element to test. + /// The point on . + /// + /// The active input elements found at the point, ordered topmost first. + /// public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { Contract.Requires(element != null); @@ -22,7 +33,7 @@ namespace Perspex.Input if (element.VisualChildren.Any()) { - foreach (var child in element.VisualChildren.OfType()) + foreach (var child in ZSort(element.VisualChildren.OfType())) { foreach (var result in child.GetInputElementsAt(p)) { @@ -34,5 +45,51 @@ namespace Perspex.Input yield return element; } } + + /// + /// Returns the topmost active input element at a point on an . + /// + /// The element to test. + /// The point on . + /// The topmost at the specified position. + public static IInputElement InputHitTest(this IInputElement element, Point p) + { + return element.GetInputElementsAt(p).First(); + } + + private static IEnumerable ZSort(IEnumerable elements) + { + return elements + .Select((element, index) => new ZOrderElement + { + Element = element, + Index = index, + ZIndex = element.ZIndex, + }) + .OrderBy(x => x, null) + .Select(x => x.Element); + + } + + private class ZOrderElement : IComparable + { + public IInputElement Element { get; set; } + public int Index { get; set; } + public int ZIndex { get; set; } + + public int CompareTo(ZOrderElement other) + { + var z = other.ZIndex - ZIndex; + + if (z != 0) + { + return z; + } + else + { + return other.Index - Index; + } + } + } } } diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index e835c6a76c..9050f76a4f 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -192,6 +192,11 @@ namespace Perspex /// /// Gets the Z index of the node. /// + /// + /// Controls with a higher will appear in front of controls with + /// a lower ZIndex. If two controls have the same ZIndex then the control that appears + /// later in the containing element's children collection will appear on top. + /// public int ZIndex { get { return GetValue(ZIndexProperty); } diff --git a/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs new file mode 100644 index 0000000000..aeb11aa413 --- /dev/null +++ b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs @@ -0,0 +1,129 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Perspex.Layout; +using Xunit; + +namespace Perspex.Input.UnitTests +{ + public class InputElement_HitTesting + { + [Fact] + public void InputHitTest_Should_Find_Control_At_Point() + { + 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 result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Child, result); + } + + [Fact] + public void InputHitTest_Should_Not_Find_Control_Outside_Point() + { + 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 result = container.InputHitTest(new Point(10, 10)); + + Assert.Equal(container, result); + } + + [Fact] + public void InputHitTest_Should_Find_Top_Control_At_Point() + { + 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 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() + { + 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 result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[0], result); + } + } +} diff --git a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj index 1aff689782..051e2a966f 100644 --- a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj +++ b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj @@ -55,6 +55,7 @@ +