diff --git a/src/Perspex.Input/Gestures.cs b/src/Perspex.Input/Gestures.cs new file mode 100644 index 0000000000..48e9d87f86 --- /dev/null +++ b/src/Perspex.Input/Gestures.cs @@ -0,0 +1,59 @@ +// 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 System; +using Perspex.Interactivity; + +namespace Perspex.Input +{ + public static class Gestures + { + public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( + "Tapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( + "DoubleTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + private static IInteractive s_lastPress; + + static Gestures() + { + InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); + InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); + } + + private static void PointerPressed(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerPressedEventArgs)ev; + + if (e.ClickCount <= 1) + { + s_lastPress = e.Source; + } + else if (e.ClickCount == 2 && s_lastPress == e.Source) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } + } + } + + private static void PointerReleased(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerReleasedEventArgs)ev; + + if (s_lastPress == e.Source) + { + s_lastPress.RaiseEvent(new RoutedEventArgs(TappedEvent)); + } + } + } + } +} diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index dab429cbdf..00e53df646 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -137,6 +137,16 @@ namespace Perspex.Input "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isFocused; private bool _isPointerOver; @@ -259,6 +269,24 @@ namespace Perspex.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } + /// + /// Occurs when a tap gesture occurs on the control. + /// + public event EventHandler Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// Occurs when a double-tap gesture occurs on the control. + /// + public event EventHandler DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + /// /// Gets or sets a value indicating whether the control can receive focus. /// diff --git a/src/Perspex.Input/Perspex.Input.csproj b/src/Perspex.Input/Perspex.Input.csproj index 264eda7473..80e1566f1c 100644 --- a/src/Perspex.Input/Perspex.Input.csproj +++ b/src/Perspex.Input/Perspex.Input.csproj @@ -68,6 +68,7 @@ + diff --git a/src/Perspex.Input/PointerEventArgs.cs b/src/Perspex.Input/PointerEventArgs.cs index fab4c2a9c6..25fbf7f26d 100644 --- a/src/Perspex.Input/PointerEventArgs.cs +++ b/src/Perspex.Input/PointerEventArgs.cs @@ -8,6 +8,17 @@ namespace Perspex.Input { public class PointerEventArgs : RoutedEventArgs { + public PointerEventArgs() + { + + } + + public PointerEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + + } + public IPointerDevice Device { get; set; } public InputModifiers InputModifiers { get; set; } @@ -28,12 +39,32 @@ namespace Perspex.Input public class PointerPressedEventArgs : PointerEventArgs { + public PointerPressedEventArgs() + : base(InputElement.PointerPressedEvent) + { + } + + public PointerPressedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public int ClickCount { get; set; } public MouseButton MouseButton { get; set; } } public class PointerReleasedEventArgs : PointerEventArgs { + public PointerReleasedEventArgs() + : base(InputElement.PointerReleasedEvent) + { + } + + public PointerReleasedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public MouseButton MouseButton { get; set; } } } diff --git a/src/Perspex.Interactivity/Interactive.cs b/src/Perspex.Interactivity/Interactive.cs index 956b2c2722..c348a3ed83 100644 --- a/src/Perspex.Interactivity/Interactive.cs +++ b/src/Perspex.Interactivity/Interactive.cs @@ -122,16 +122,19 @@ namespace Perspex.Interactivity { e.Route = RoutingStrategies.Direct; RaiseEventImpl(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) { TunnelEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) { BubbleEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } } diff --git a/src/Perspex.Interactivity/RoutedEvent.cs b/src/Perspex.Interactivity/RoutedEvent.cs index 3040c867a4..9b5dee61f5 100644 --- a/src/Perspex.Interactivity/RoutedEvent.cs +++ b/src/Perspex.Interactivity/RoutedEvent.cs @@ -19,6 +19,7 @@ namespace Perspex.Interactivity public class RoutedEvent { private Subject> _raised = new Subject>(); + private Subject _routeFinished = new Subject(); public RoutedEvent( string name, @@ -62,6 +63,7 @@ namespace Perspex.Interactivity } public IObservable> Raised => _raised; + public IObservable RouteFinished => _routeFinished; public static RoutedEvent Register( string name, @@ -116,6 +118,11 @@ namespace Perspex.Interactivity { _raised.OnNext(Tuple.Create(sender, e)); } + + internal void InvokeRouteFinished(RoutedEventArgs e) + { + _routeFinished.OnNext(e); + } } public class RoutedEvent : RoutedEvent diff --git a/tests/Perspex.Interactivity.UnitTests/GestureTests.cs b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs new file mode 100644 index 0000000000..9712ea480e --- /dev/null +++ b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs @@ -0,0 +1,82 @@ +// 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 System.Collections.Generic; +using Perspex.Controls; +using Perspex.Input; +using Xunit; + +namespace Perspex.Interactivity.UnitTests +{ + public class GestureTests + { + [Fact] + public void Tapped_Should_Follow_Pointer_Pressed_Released() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bt", "dt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + } +} diff --git a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj index 69137a9d35..9532487e13 100644 --- a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj +++ b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj @@ -59,6 +59,7 @@ + @@ -74,6 +75,14 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Perspex.Base + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} Perspex.Interactivity @@ -86,6 +95,10 @@ {EB582467-6ABB-43A1-B052-E981BA910E3A} Perspex.SceneGraph + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling +