// 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 System; using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Represents a mouse device. /// public class MouseDevice : IMouseDevice { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private IInputElement _captured; private IDisposable _capturedSubscription; /// /// Gets the control that is currently capturing by the mouse, if any. /// /// /// When an element captures the mouse, it receives mouse input whether the cursor is /// within the control's bounds or not. To set the mouse capture, call the /// method. /// public IInputElement Captured { get => _captured; protected set { _capturedSubscription?.Dispose(); _capturedSubscription = null; if (value != null) { _capturedSubscription = Observable.FromEventPattern( x => value.DetachedFromVisualTree += x, x => value.DetachedFromVisualTree -= x) .Take(1) .Subscribe(_ => Captured = null); } _captured = value; } } /// /// Gets the mouse position, in screen coordinates. /// public PixelPoint Position { get; protected set; } /// /// Captures mouse input to the specified control. /// /// The control. /// /// When an element captures the mouse, it receives mouse input whether the cursor is /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// public virtual void Capture(IInputElement control) { // TODO: Check visibility and enabled state before setting capture. Captured = control; } /// /// Gets the mouse position relative to a control. /// /// The control. /// The mouse position in the control's coordinates. public Point GetPosition(IVisual relativeTo) { Contract.Requires(relativeTo != null); if (relativeTo.VisualRoot == null) { throw new InvalidOperationException("Control is not attached to visual tree."); } var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform.Value; } public void ProcessRawEvent(RawInputEventArgs e) { if (!e.Handled && e is RawMouseEventArgs margs) ProcessRawEvent(margs); } public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); if (rect.Contains(clientPoint)) { if (Captured == null) { SetPointerOver(this, root, clientPoint, InputModifiers.None); } else { SetPointerOver(this, root, Captured, InputModifiers.None); } } } private void ProcessRawEvent(RawMouseEventArgs e) { Contract.Requires(e != null); var mouse = (IMouseDevice)e.Device; Position = e.Root.PointToScreen(e.Position); switch (e.Type) { case RawMouseEventType.LeaveWindow: LeaveWindow(mouse, e.Root, e.InputModifiers); break; case RawMouseEventType.LeftButtonDown: case RawMouseEventType.RightButtonDown: case RawMouseEventType.MiddleButtonDown: e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Type == RawMouseEventType.LeftButtonDown ? MouseButton.Left : e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle, e.InputModifiers); break; case RawMouseEventType.LeftButtonUp: case RawMouseEventType.RightButtonUp: case RawMouseEventType.MiddleButtonUp: e.Handled = MouseUp(mouse, e.Root, e.Position, e.Type == RawMouseEventType.LeftButtonUp ? MouseButton.Left : e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle, e.InputModifiers); break; case RawMouseEventType.Move: e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers); break; case RawMouseEventType.Wheel: e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); break; } } private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); ClearPointerOver(this, root, inputModifiers); } private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { IInteractive source = GetSource(hit); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); var doubleClickTime = settings.DoubleClickTime.TotalMilliseconds; if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) { _clickCount = 0; } ++_clickCount; _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); var e = new PointerPressedEventArgs { Device = this, RoutedEvent = InputElement.PointerPressedEvent, Source = source, ClickCount = _clickCount, MouseButton = button, InputModifiers = inputModifiers }; source.RaiseEvent(e); return e.Handled; } } return false; } private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); IInputElement source; if (Captured == null) { source = SetPointerOver(this, root, p, inputModifiers); } else { SetPointerOver(this, root, Captured, inputModifiers); source = Captured; } var e = new PointerEventArgs { Device = this, RoutedEvent = InputElement.PointerMovedEvent, Source = source, InputModifiers = inputModifiers }; source?.RaiseEvent(e); return e.Handled; } private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { var source = GetSource(hit); var e = new PointerReleasedEventArgs { Device = this, RoutedEvent = InputElement.PointerReleasedEvent, Source = source, MouseButton = button, InputModifiers = inputModifiers }; source?.RaiseEvent(e); return e.Handled; } return false; } private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { var source = GetSource(hit); var e = new PointerWheelEventArgs { Device = this, RoutedEvent = InputElement.PointerWheelChangedEvent, Source = source, Delta = delta, InputModifiers = inputModifiers }; source?.RaiseEvent(e); return e.Handled; } return false; } private IInteractive GetSource(IVisual hit) { Contract.Requires(hit != null); return Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } private IInputElement HitTest(IInputElement root, Point p) { Contract.Requires(root != null); return Captured ?? root.InputHitTest(p); } private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; var e = new PointerEventArgs { RoutedEvent = InputElement.PointerLeaveEvent, Device = device, InputModifiers = inputModifiers }; if (element!=null && !element.IsAttachedToVisualTree) { // element has been removed from visual tree so do top down cleanup if (root.IsPointerOver) ClearChildrenPointerOver(e, root,true); } while (element != null) { e.Source = element; e.Handled = false; element.RaiseEvent(e); element = (IInputElement)element.VisualParent; } root.PointerOverElement = null; } private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) { foreach (IInputElement el in element.VisualChildren) { if (el.IsPointerOver) { ClearChildrenPointerOver(e, el, true); break; } } if(clearRoot) { e.Source = element; e.Handled = false; element.RaiseEvent(e); } } private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.InputHitTest(p); if (element != root.PointerOverElement) { if (element != null) { SetPointerOver(device, root, element, inputModifiers); } else { ClearPointerOver(device, root, inputModifiers); } } return element; } private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); Contract.Requires(element != null); IInputElement branch = null; var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers }; var el = element; while (el != null) { if (el.IsPointerOver) { branch = el; break; } el = (IInputElement)el.VisualParent; } el = root.PointerOverElement; e.RoutedEvent = InputElement.PointerLeaveEvent; if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); } while (el != null && el != branch) { e.Source = el; e.Handled = false; el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } el = root.PointerOverElement = element; e.RoutedEvent = InputElement.PointerEnterEvent; while (el != null && el != branch) { e.Source = el; e.Handled = false; el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } } } }