Explorar o código

Fixed pointerover state.

And added a test for it.
Steven Kirk %!s(int64=9) %!d(string=hai) anos
pai
achega
349ba78931

+ 3 - 3
src/Avalonia.Input/IInputElement.cs

@@ -95,17 +95,17 @@ namespace Avalonia.Input
         bool IsEnabledCore { get; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the control is focused.
+        /// Gets a value indicating whether the control is focused.
         /// </summary>
         bool IsFocused { get; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the control is considered for hit testing.
+        /// Gets a value indicating whether the control is considered for hit testing.
         /// </summary>
         bool IsHitTestVisible { get; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the pointer is currently over the control.
+        /// Gets a value indicating whether the pointer is currently over the control.
         /// </summary>
         bool IsPointerOver { get; }
 

+ 86 - 35
src/Avalonia.Input/MouseDevice.cs

@@ -20,7 +20,6 @@ namespace Avalonia.Input
         private int _clickCount;
         private Rect _lastClickRect;
         private uint _lastClickTime;
-        private readonly List<IInputElement> _pointerOvers = new List<IInputElement>();
 
         /// <summary>
         /// Intializes a new instance of <see cref="MouseDevice"/>.
@@ -87,6 +86,8 @@ namespace Avalonia.Input
         /// <returns>The mouse position in the control's coordinates.</returns>
         public Point GetPosition(IVisual relativeTo)
         {
+            Contract.Requires<ArgumentNullException>(relativeTo != null);
+
             Point p = default(Point);
             IVisual v = relativeTo;
             IVisual root = null;
@@ -103,6 +104,8 @@ namespace Avalonia.Input
 
         private void ProcessRawEvent(RawMouseEventArgs e)
         {
+            Contract.Requires<ArgumentNullException>(e != null);
+
             var mouse = (IMouseDevice)e.Device;
 
             Position = e.Root.PointToScreen(e.Position);
@@ -141,11 +144,17 @@ namespace Avalonia.Input
 
         private void LeaveWindow(IMouseDevice device, IInputRoot root)
         {
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
             ClearPointerOver(this, root);
         }
 
         private bool MouseDown(IMouseDevice device, uint timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers)
         {
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
             var hit = HitTest(root, p);
 
             if (hit != null)
@@ -187,6 +196,9 @@ namespace Avalonia.Input
 
         private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
         {
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
             IInputElement source;
 
             if (Captured == null)
@@ -195,8 +207,7 @@ namespace Avalonia.Input
             }
             else
             {
-                var elements = Captured.GetSelfAndVisualAncestors().OfType<IInputElement>().ToList();
-                SetPointerOver(this, root, elements);
+                SetPointerOver(this, root, Captured);
                 source = Captured;
             }
 
@@ -214,6 +225,9 @@ namespace Avalonia.Input
 
         private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers)
         {
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
             var hit = HitTest(root, p);
 
             if (hit != null)
@@ -237,6 +251,9 @@ namespace Avalonia.Input
 
         private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers)
         {
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
             var hit = HitTest(root, p);
 
             if (hit != null)
@@ -260,6 +277,8 @@ namespace Avalonia.Input
 
         private IInteractive GetSource(IVisual hit)
         {
+            Contract.Requires<ArgumentNullException>(hit != null);
+
             return Captured ??
                 (hit as IInteractive) ??
                 hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
@@ -267,22 +286,28 @@ namespace Avalonia.Input
 
         private IInputElement HitTest(IInputElement root, Point p)
         {
+            Contract.Requires<ArgumentNullException>(root != null);
+
             return Captured ?? root.InputHitTest(p);
         }
 
         private void ClearPointerOver(IPointerDevice device, IInputRoot root)
         {
-            foreach (var control in _pointerOvers.ToList())
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            var element = root.PointerOverElement;
+            var e = new PointerEventArgs
             {
-                PointerEventArgs e = new PointerEventArgs
-                {
-                    RoutedEvent = InputElement.PointerLeaveEvent,
-                    Device = device,
-                    Source = control,
-                };
+                RoutedEvent = InputElement.PointerLeaveEvent,
+                Device = device,
+            };
 
-                _pointerOvers.Remove(control);
-                control.RaiseEvent(e);
+            while (element != null)
+            {
+                e.Source = element;
+                element.RaiseEvent(e);
+                element = (IInputElement)element.VisualParent;
             }
 
             root.PointerOverElement = null;
@@ -290,40 +315,66 @@ namespace Avalonia.Input
 
         private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p)
         {
-            var elements = root.GetInputElementsAt(p).ToList();
-            return SetPointerOver(device, root, elements);
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            var element = root.InputHitTest(p);
+
+            if (element != root.PointerOverElement)
+            {
+                if (element != null)
+                {
+                    SetPointerOver(device, root, element);
+                }
+                else
+                {
+                    ClearPointerOver(device, root);
+                }
+            }
+
+            return element;
         }
 
-        private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, IList<IInputElement> elements)
+        private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element)
         {
-            foreach (var control in _pointerOvers.Except(elements).ToList())
+            Contract.Requires<ArgumentNullException>(device != null);
+            Contract.Requires<ArgumentNullException>(root != null);
+            Contract.Requires<ArgumentNullException>(element != null);
+
+            IInputElement branch = null;
+
+            var e = new PointerEventArgs
             {
-                PointerEventArgs e = new PointerEventArgs
-                {
-                    RoutedEvent = InputElement.PointerLeaveEvent,
-                    Device = device,
-                    Source = control,
-                };
+                RoutedEvent = InputElement.PointerEnterEvent,
+                Device = device,
+            };
 
-                _pointerOvers.Remove(control);
-                control.RaiseEvent(e);
-            }
+            var el = element;
 
-            foreach (var control in elements.Except(_pointerOvers))
+            while (el != null)
             {
-                PointerEventArgs e = new PointerEventArgs
+                if (el.IsPointerOver)
                 {
-                    RoutedEvent = InputElement.PointerEnterEvent,
-                    Device = device,
-                    Source = control,
-                };
+                    branch = el;
+                    break;
+                }
 
-                _pointerOvers.Add(control);
-                control.RaiseEvent(e);
+                e.Source = el;
+                el.RaiseEvent(e);
+                el = (IInputElement)el.VisualParent;
+            }
+
+            el = root.PointerOverElement;
+            e.RoutedEvent = InputElement.PointerLeaveEvent;
+
+            while (el != null && el != branch)
+            {
+                e.Source = el;
+                el.RaiseEvent(e);
+                el = (IInputElement)el.VisualParent;
             }
 
-            root.PointerOverElement = elements.FirstOrDefault() ?? root;
-            return root.PointerOverElement;
+            root.PointerOverElement = element;
         }
     }
 }

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

@@ -89,6 +89,7 @@
     <Compile Include="KeyboardNavigationTests_Arrows.cs" />
     <Compile Include="KeyboardNavigationTests_Tab.cs" />
     <Compile Include="KeyGestureParseTests.cs" />
+    <Compile Include="MouseDeviceTests.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <ItemGroup>

+ 87 - 0
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@@ -0,0 +1,87 @@
+using Avalonia.Controls;
+using Avalonia.Input.Raw;
+using Avalonia.Layout;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using System;
+using Xunit;
+
+namespace Avalonia.Input.UnitTests
+{
+    public class MouseDeviceTests
+    {
+        [Fact]
+        public void MouseMove_Should_Update_PointerOver()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (TestApplication(renderer.Object))
+            {
+                var inputManager = InputManager.Instance;
+                var mouseDevice = AvaloniaLocator.Current.GetService<IMouseDevice>();
+
+                Canvas canvas;
+                Border border;
+                Decorator decorator;
+
+                var root = new TestRoot
+                {
+                    Child = new Panel
+                    {
+                        Children =
+                        {
+                            (canvas = new Canvas()),
+                            (border = new Border
+                            {
+                                Child = decorator = new Decorator(),
+                            })
+                        }
+                    }
+                };
+
+                renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
+                    .Returns(new[] { decorator });
+
+                inputManager.ProcessInput(new RawMouseEventArgs(
+                    mouseDevice,
+                    0,
+                    root,
+                    RawMouseEventType.Move,
+                    new Point(),
+                    InputModifiers.None));
+
+                Assert.True(decorator.IsPointerOver);
+                Assert.True(border.IsPointerOver);
+                Assert.False(canvas.IsPointerOver);
+                Assert.True(root.IsPointerOver);
+
+                renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
+                    .Returns(new[] { canvas });
+
+                inputManager.ProcessInput(new RawMouseEventArgs(
+                    mouseDevice,
+                    0,
+                    root,
+                    RawMouseEventType.Move,
+                    new Point(),
+                    InputModifiers.None));
+
+                Assert.False(decorator.IsPointerOver);
+                Assert.False(border.IsPointerOver);
+                Assert.True(canvas.IsPointerOver);
+                Assert.True(root.IsPointerOver);
+            }
+        }
+
+        private IDisposable TestApplication(IRenderer renderer)
+        {
+            return UnitTestApplication.Start(
+                new TestServices(
+                    inputManager: new InputManager(),
+                    mouseDevice: () => new MouseDevice(),
+                    renderer: (root, loop) => renderer));
+        }
+    }
+}

+ 21 - 1
tests/Avalonia.UnitTests/TestRoot.cs

@@ -11,7 +11,7 @@ using Avalonia.Styling;
 
 namespace Avalonia.UnitTests
 {
-    public class TestRoot : Decorator, IFocusScope, ILayoutRoot, INameScope, IRenderRoot, IStyleRoot
+    public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, INameScope, IRenderRoot, IStyleRoot
     {
         private readonly NameScope _nameScope = new NameScope();
 
@@ -50,6 +50,26 @@ namespace Avalonia.UnitTests
 
         public IRenderer Renderer { get; }
 
+        public IAccessKeyHandler AccessKeyHandler
+        {
+            get
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        public IKeyboardNavigationHandler KeyboardNavigationHandler
+        {
+            get
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        public IInputElement PointerOverElement { get; set; }
+
+        public bool ShowAccessKeys { get; set; }
+
         public IRenderTarget CreateRenderTarget()
         {
             throw new NotImplementedException();

+ 5 - 0
tests/Avalonia.UnitTests/TestServices.cs

@@ -62,6 +62,7 @@ namespace Avalonia.UnitTests
             IInputManager inputManager = null,
             Func<IKeyboardDevice> keyboardDevice = null,
             ILayoutManager layoutManager = null,
+            Func<IMouseDevice> mouseDevice = null,
             IRuntimePlatform platform = null,
             Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
             IPlatformRenderInterface renderInterface = null,
@@ -78,6 +79,7 @@ namespace Avalonia.UnitTests
             InputManager = inputManager;
             KeyboardDevice = keyboardDevice;
             LayoutManager = layoutManager;
+            MouseDevice = mouseDevice;
             Platform = platform;
             Renderer = renderer;
             RenderInterface = renderInterface;
@@ -95,6 +97,7 @@ namespace Avalonia.UnitTests
         public IFocusManager FocusManager { get; }
         public Func<IKeyboardDevice> KeyboardDevice { get; }
         public ILayoutManager LayoutManager { get; }
+        public Func<IMouseDevice> MouseDevice { get; }
         public IRuntimePlatform Platform { get; }
         public Func<IRenderRoot, IRenderLoop, IRenderer> Renderer { get; }
         public IPlatformRenderInterface RenderInterface { get; }
@@ -112,6 +115,7 @@ namespace Avalonia.UnitTests
             IInputManager inputManager = null,
             Func<IKeyboardDevice> keyboardDevice = null,
             ILayoutManager layoutManager = null,
+            Func<IMouseDevice> mouseDevice = null,
             IRuntimePlatform platform = null,
             Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
             IPlatformRenderInterface renderInterface = null,
@@ -129,6 +133,7 @@ namespace Avalonia.UnitTests
                 inputManager: inputManager ?? InputManager,
                 keyboardDevice: keyboardDevice ?? KeyboardDevice,
                 layoutManager: layoutManager ?? LayoutManager,
+                mouseDevice: mouseDevice ?? MouseDevice,
                 platform: platform ?? Platform,
                 renderer: renderer ?? Renderer,
                 renderInterface: renderInterface ?? RenderInterface,

+ 1 - 0
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -42,6 +42,7 @@ namespace Avalonia.UnitTests
                 .Bind<IInputManager>().ToConstant(Services.InputManager)
                 .Bind<IKeyboardDevice>().ToConstant(Services.KeyboardDevice?.Invoke())
                 .Bind<ILayoutManager>().ToConstant(Services.LayoutManager)
+                .Bind<IMouseDevice>().ToConstant(Services.MouseDevice?.Invoke())
                 .Bind<IRuntimePlatform>().ToConstant(Services.Platform)
                 .Bind<IRendererFactory>().ToConstant(new RendererFactory(Services.Renderer))
                 .Bind<IPlatformRenderInterface>().ToConstant(Services.RenderInterface)