diff --git a/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs
new file mode 100644
index 0000000000..02fd800baf
--- /dev/null
+++ b/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs
@@ -0,0 +1,124 @@
+using System.Timers;
+using Avalonia.Input.GestureRecognizers;
+using Avalonia.Threading;
+
+namespace Avalonia.Input
+{
+ public class HoldGestureRecognizer : StyledElement, IGestureRecognizer
+ {
+ private const int Tolerance = 30;
+ private IInputElement? _target;
+ private IGestureRecognizerActionsDispatcher? _actions;
+ private int _gestureId;
+ private IPointer? _tracking;
+ private PointerPressedEventArgs? _pointerEventArgs;
+ private Rect _trackingBounds;
+ private Timer? _holdTimer;
+ private bool _elasped;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsHoldWithMouseEnabledProperty =
+ AvaloniaProperty.Register(
+ nameof(IsHoldWithMouseEnabled));
+
+ ///
+ /// Gets or sets whether to detect hold from the mouse
+ ///
+ public bool IsHoldWithMouseEnabled
+ {
+ get => GetValue(IsHoldWithMouseEnabledProperty);
+ set => SetValue(IsHoldWithMouseEnabledProperty, value);
+ }
+
+ public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
+ {
+ _target = target;
+ _actions = actions;
+
+ _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+ _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+
+ _holdTimer = new Timer(300);
+ _holdTimer.AutoReset = false;
+ _holdTimer.Elapsed += HoldTimer_Elapsed;
+ }
+
+ private async void HoldTimer_Elapsed(object? sender, ElapsedEventArgs e)
+ {
+ _elasped = true;
+ _holdTimer?.Stop();
+
+ if(_tracking != null)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() => _target?.RaiseEvent(new HoldGestureEventArgs(_gestureId, _pointerEventArgs, HoldingState.Started)));
+ }
+ }
+
+ private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ PointerPressed(e);
+ }
+
+ private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ PointerReleased(e);
+ }
+
+ public void PointerCaptureLost(IPointer pointer)
+ {
+ if (_tracking == pointer)
+ {
+ EndHold(!_elasped);
+ }
+ }
+
+ public void PointerMoved(PointerEventArgs e)
+ {
+ if (_tracking == e.Pointer && _target is Visual visual)
+ {
+ var currentPosition = e.GetPosition(visual);
+
+ if (!_trackingBounds.Contains(currentPosition))
+ {
+ EndHold(true);
+ }
+ }
+ }
+
+ public void PointerPressed(PointerPressedEventArgs e)
+ {
+ if (_target != null && _target is Visual visual && (IsHoldWithMouseEnabled || e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
+ {
+ _elasped = false;
+ var position = e.GetPosition(visual);
+ _gestureId = HoldGestureEventArgs.GetNextFreeId();
+ _tracking = e.Pointer;
+ _pointerEventArgs = e;
+
+ _trackingBounds = new Rect(position.X - Tolerance / 2, position.Y - Tolerance / 2, Tolerance, Tolerance);
+
+ _holdTimer?.Start();
+ }
+ }
+
+ public void PointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_tracking == e.Pointer)
+ {
+ EndHold(!_elasped);
+ }
+ }
+
+ private void EndHold(bool cancelled)
+ {
+ _holdTimer?.Stop();
+
+ _tracking = null;
+ _trackingBounds = default;
+
+ _target?.RaiseEvent(new HoldGestureEventArgs(_gestureId, _pointerEventArgs, cancelled ? HoldingState.Cancelled : HoldingState.Completed));
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs
index b4d5feaf3b..1c2220baf4 100644
--- a/src/Avalonia.Base/Input/Gestures.cs
+++ b/src/Avalonia.Base/Input/Gestures.cs
@@ -58,6 +58,10 @@ namespace Avalonia.Input
RoutedEvent.Register(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
+ public static readonly RoutedEvent HoldGestureEvent =
+ RoutedEvent.Register(
+ "HoldGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
public static readonly RoutedEvent PullGestureEndedEvent =
RoutedEvent.Register(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
diff --git a/src/Avalonia.Base/Input/HoldGestureEventArgs.cs b/src/Avalonia.Base/Input/HoldGestureEventArgs.cs
new file mode 100644
index 0000000000..63a508b1f4
--- /dev/null
+++ b/src/Avalonia.Base/Input/HoldGestureEventArgs.cs
@@ -0,0 +1,31 @@
+using System;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+ public class HoldGestureEventArgs : RoutedEventArgs
+ {
+ public int Id { get; }
+ public Vector Delta { get; }
+ public HoldingState HoldingState { get; }
+ public PointerEventArgs? PointerEventArgs { get; }
+
+ private static int _nextId = 1;
+
+ internal static int GetNextFreeId() => _nextId++;
+
+ public HoldGestureEventArgs(int id, PointerEventArgs? pointerEventArgs, HoldingState holdingState) : base(Gestures.HoldGestureEvent)
+ {
+ Id = id;
+ HoldingState = holdingState;
+ PointerEventArgs = pointerEventArgs;
+ }
+ }
+
+ public enum HoldingState
+ {
+ Started,
+ Completed,
+ Cancelled,
+ }
+}
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 88c9823952..de91fb83c9 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -216,6 +216,12 @@ namespace Avalonia.Controls
///
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
+ public Control()
+ {
+ // Add a default HoldGestureRecognizer
+ GestureRecognizers.Add(new HoldGestureRecognizer());
+ }
+
///
void ISetterValue.Initialize(ISetter setter)
{
@@ -366,16 +372,29 @@ namespace Avalonia.Controls
{
base.OnAttachedToVisualTreeCore(e);
+ AddHandler(Gestures.HoldGestureEvent, OnHoldEvent);
+
InitializeIfNeeded();
ScheduleOnLoadedCore();
}
+ private void OnHoldEvent(object? sender, HoldGestureEventArgs e)
+ {
+ if(e.HoldingState == HoldingState.Started)
+ {
+ // Trigger ContentRequest when hold starts
+ RaiseEvent(new ContextRequestedEventArgs(e.PointerEventArgs!));
+ }
+ }
+
///
protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTreeCore(e);
+ RemoveHandler(Gestures.HoldGestureEvent, OnHoldEvent);
+
OnUnloadedCore();
}