Browse Source

Fixed input hit test Z ordering.

Closes #170.
Steven Kirk 10 years ago
parent
commit
92e6274ec3

+ 1 - 7
src/Perspex.Input/IInputElement.cs

@@ -114,14 +114,8 @@ namespace Perspex.Input
         void Focus();
 
         /// <summary>
-        /// Returns the input element that can be found within the current control at the specified
-        /// position.
+        /// Gets the key bindings for the element.
         /// </summary>
-        /// <param name="p">The position, in control coordinates.</param>
-        /// <returns>The <see cref="IInputElement"/> at the specified position.</returns>
-        IInputElement InputHitTest(Point p);
-
-
         List<KeyBinding> KeyBindings { get; }
     }
 }

+ 0 - 11
src/Perspex.Input/InputElement.cs

@@ -339,17 +339,6 @@ namespace Perspex.Input
 
         public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
 
-        /// <summary>
-        /// Returns the input element that can be found within the current control at the specified
-        /// position.
-        /// </summary>
-        /// <param name="p">The position, in control coordinates.</param>
-        /// <returns>The <see cref="IInputElement"/> at the specified position.</returns>
-        public IInputElement InputHitTest(Point p)
-        {
-            return this.GetInputElementsAt(p).FirstOrDefault();
-        }
-
         /// <summary>
         /// Focuses the control.
         /// </summary>

+ 58 - 1
src/Perspex.Input/InputExtensions.cs

@@ -7,8 +7,19 @@ using System.Linq;
 
 namespace Perspex.Input
 {
+    /// <summary>
+    /// Defines extensions for the <see cref="IInputElement"/> interface.
+    /// </summary>
     public static class InputExtensions
     {
+        /// <summary>
+        /// Returns the active input elements at a point on an <see cref="IInputElement"/>.
+        /// </summary>
+        /// <param name="element">The element to test.</param>
+        /// <param name="p">The point on <paramref name="element"/>.</param>
+        /// <returns>
+        /// The active input elements found at the point, ordered topmost first.
+        /// </returns>
         public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
         {
             Contract.Requires<ArgumentNullException>(element != null);
@@ -22,7 +33,7 @@ namespace Perspex.Input
 
                 if (element.VisualChildren.Any())
                 {
-                    foreach (var child in element.VisualChildren.OfType<IInputElement>())
+                    foreach (var child in ZSort(element.VisualChildren.OfType<IInputElement>()))
                     {
                         foreach (var result in child.GetInputElementsAt(p))
                         {
@@ -34,5 +45,51 @@ namespace Perspex.Input
                 yield return element;
             }
         }
+
+        /// <summary>
+        /// Returns the topmost active input element at a point on an <see cref="IInputElement"/>.
+        /// </summary>
+        /// <param name="element">The element to test.</param>
+        /// <param name="p">The point on <paramref name="element"/>.</param>
+        /// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
+        public static IInputElement InputHitTest(this IInputElement element, Point p)
+        {
+            return element.GetInputElementsAt(p).First();
+        }
+
+        private static IEnumerable<IInputElement> ZSort(IEnumerable<IInputElement> 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<ZOrderElement>
+        {
+            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;
+                }
+            }
+        }
     }
 }

+ 5 - 0
src/Perspex.SceneGraph/Visual.cs

@@ -192,6 +192,11 @@ namespace Perspex
         /// <summary>
         /// Gets the Z index of the node.
         /// </summary>
+        /// <remarks>
+        /// Controls with a higher <see cref="ZIndex"/> 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.
+        /// </remarks>
         public int ZIndex
         {
             get { return GetValue(ZIndexProperty); }

+ 129 - 0
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);
+        }
+    }
+}

+ 1 - 0
tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj

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