Skip to content

Commit 5af1a7a

Browse files
RedthCopilot
andcommitted
Make LongPressGestureRecognizer dispatch API public for platform backends
External platform backends (for example Maui.Tizen) can raise tap and pointer gestures because TapGestureRecognizer.SendTapped and PointerGestureRecognizer.Send* are public infrastructure APIs. LongPressGestureRecognizer.SendLongPressing and SendLongPressed were still internal, so an out-of-tree backend could detect a long press but had no supported way to dispatch it. Promote both methods to public with [EditorBrowsable(EditorBrowsableState.Never)] and an ArgumentNullException guard on sender, matching the existing tap/pointer dispatch APIs exactly. Command execution, event raising, GestureStatus transitions, State updates, position callback behavior, and all in-box platform call sites are unchanged. Adds src/Controls/tests/ExternalGestureBackend, a test-support assembly that is deliberately excluded from Microsoft.Maui.Controls' InternalsVisibleTo list, so its fake backend can only compile against the public surface. Demoting either method back to internal breaks that project's build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent bedd1b1 commit 5af1a7a

14 files changed

Lines changed: 559 additions & 6 deletions
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Gesture dispatch for external platform backends
2+
3+
.NET MAUI ships gesture *recognizers* (`TapGestureRecognizer`, `PointerGestureRecognizer`,
4+
`LongPressGestureRecognizer`, …) in `Microsoft.Maui.Controls`, and gesture *detection* in the
5+
per-platform gesture managers under `src/Controls/src/Core/Platform`.
6+
7+
Out-of-tree platform backends — for example [`Maui.Tizen`](https://github.com/Redth/Maui.Tizen)
8+
implement their own detection layer and need to raise the recognizer's events. To make that
9+
possible, each recognizer exposes an infrastructure dispatch surface: `public` methods annotated
10+
with `[EditorBrowsable(EditorBrowsableState.Never)]` so they are reachable from any assembly but
11+
stay out of IntelliSense for app authors.
12+
13+
| Recognizer | Dispatch API |
14+
| ---------- | ------------ |
15+
| `TapGestureRecognizer` | `SendTapped(View sender, Func<IElement?, Point?>? getPosition = null)` |
16+
| `PointerGestureRecognizer` | `SendPointerEntered` / `SendPointerExited` / `SendPointerMoved` / `SendPointerPressed` / `SendPointerReleased` |
17+
| `LongPressGestureRecognizer` | `SendLongPressing(View sender, GestureStatus status, Func<IElement?, Point?>? getPosition = null)` and `SendLongPressed(View sender, Func<IElement?, Point?>? getPosition = null)` |
18+
| `DragGestureRecognizer` | `SendDragStarting` / `SendDropCompleted` |
19+
| `DropGestureRecognizer` | `SendDragLeave` / `SendDrop` |
20+
21+
## Rules that apply to every dispatch method
22+
23+
- **`sender` must not be `null`.** Each method throws `ArgumentNullException`; pass the `View` the
24+
gesture was detected on.
25+
- **Call on the UI thread.** Dispatch runs user event handlers and commands synchronously, and it
26+
writes bindable properties. Marshal to the dispatcher before calling.
27+
- **`getPosition` is optional.** Pass `null` when the platform cannot supply a location. Otherwise
28+
supply a callback that returns the gesture location *relative to the element passed in*, and the
29+
location relative to `sender` when the argument is `null` or is `sender` itself. The callback is
30+
invoked lazily by the event args, so keep it cheap and avoid capturing strong references to
31+
platform views that may already be disposed.
32+
- **Only dispatch to recognizers attached to the view.** Enumerate
33+
`view.GestureRecognizers.OfType<TRecognizer>()` and honor recognizer configuration such as
34+
`NumberOfTouchesRequired`.
35+
36+
## `LongPressGestureRecognizer` contract
37+
38+
`LongPressGestureRecognizer` has two channels:
39+
40+
- `SendLongPressing` sets `LongPressGestureRecognizer.State` and raises `LongPressing` with a
41+
`GestureStatus`. It is the state machine.
42+
- `SendLongPressed` executes `Command` (when `Command.CanExecute(CommandParameter)` is `true`) and
43+
raises `LongPressed`. It is the "the long press succeeded" notification and carries
44+
`CommandParameter` on the event args.
45+
46+
Backends should follow the same ordering the in-box platforms use:
47+
48+
| Platform signal | Calls |
49+
| --------------- | ----- |
50+
| Press recognized | `SendLongPressing(view, GestureStatus.Started, getPosition)` |
51+
| Press held / moved within `AllowableMovement` | `SendLongPressing(view, GestureStatus.Running, getPosition)` |
52+
| Press released after the minimum duration | `SendLongPressed(view, getPosition)` **then** `SendLongPressing(view, GestureStatus.Completed, getPosition)` |
53+
| Press cancelled or failed | `SendLongPressing(view, GestureStatus.Canceled, getPosition)` |
54+
55+
Notes:
56+
57+
- `SendLongPressed` is raised **before** `Completed` so handlers observing `LongPressed` see the
58+
gesture result before the terminal state change.
59+
- A cancelled gesture must **not** raise `LongPressed` and must **not** execute `Command`.
60+
- `Started`/`Running` are optional for platforms that only surface a single "long press happened"
61+
callback. Android and Windows go straight to `SendLongPressed` + `Completed`.
62+
- `MinimumPressDuration`, `NumberOfTouchesRequired`, and `AllowableMovement` are *inputs* for the
63+
backend's detector. MAUI does not enforce them; the backend decides which of them the platform can
64+
honor and documents the gaps.
65+
66+
### Example backend
67+
68+
```csharp
69+
using System;
70+
using System.Linq;
71+
using Microsoft.Maui;
72+
using Microsoft.Maui.Controls;
73+
using Microsoft.Maui.Graphics;
74+
75+
public sealed class MyPlatformLongPressHandler
76+
{
77+
readonly View _view;
78+
79+
public MyPlatformLongPressHandler(View view) => _view = view;
80+
81+
Func<IElement?, Point?> GetPosition(Point origin) =>
82+
relativeTo => relativeTo is null || ReferenceEquals(relativeTo, _view)
83+
? origin
84+
: TranslateToElement(origin, relativeTo);
85+
86+
void OnPlatformLongPress(PlatformLongPressArgs e)
87+
{
88+
var position = GetPosition(new Point(e.X, e.Y));
89+
90+
foreach (var recognizer in _view.GestureRecognizers
91+
.OfType<LongPressGestureRecognizer>()
92+
.Where(r => r.NumberOfTouchesRequired == e.TouchCount))
93+
{
94+
switch (e.State)
95+
{
96+
case PlatformGestureState.Began:
97+
recognizer.SendLongPressing(_view, GestureStatus.Started, position);
98+
break;
99+
case PlatformGestureState.Changed:
100+
recognizer.SendLongPressing(_view, GestureStatus.Running, position);
101+
break;
102+
case PlatformGestureState.Ended:
103+
recognizer.SendLongPressed(_view, position);
104+
recognizer.SendLongPressing(_view, GestureStatus.Completed, position);
105+
break;
106+
case PlatformGestureState.Cancelled:
107+
case PlatformGestureState.Failed:
108+
recognizer.SendLongPressing(_view, GestureStatus.Canceled, position);
109+
break;
110+
}
111+
}
112+
}
113+
}
114+
```
115+
116+
## Keeping the contract honest
117+
118+
`src/Controls/tests/ExternalGestureBackend` is a test-support assembly that is deliberately **not**
119+
listed in `Microsoft.Maui.Controls`'s `InternalsVisibleTo` set. It contains a fake backend that
120+
drives `LongPressGestureRecognizer` purely through the public dispatch API, so demoting any of these
121+
methods back to `internal` breaks the build. `LongPressGestureRecognizerExternalBackendTests` in
122+
`Controls.Core.UnitTests` exercises that fake backend across the full
123+
`Started``Running``Completed` / `Canceled` lifecycle.
124+
125+
Add the same coverage whenever a new recognizer gains a dispatch API.

src/Controls/src/Core/LongPressGestureRecognizer.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.ComponentModel;
23
using System.Windows.Input;
34
using Microsoft.Maui.Graphics;
45

@@ -120,10 +121,22 @@ public GestureStatus State
120121
public event EventHandler<LongPressingEventArgs>? LongPressing;
121122

122123
/// <summary>
123-
/// Sends the long pressed event and executes the command.
124+
/// Executes the associated <see cref="Command"/> and raises the <see cref="LongPressed"/> event.
124125
/// </summary>
125-
internal void SendLongPressed(View sender, Func<IElement?, Point?>? getPosition = null)
126+
/// <param name="sender">The view on which the long press gesture was recognized.</param>
127+
/// <param name="getPosition">A function that returns the long press position relative to a specified element.</param>
128+
/// <remarks>
129+
/// <para>This infrastructure method is intended for gesture platform managers.</para>
130+
/// <para>Platform backends should raise this when the press completes successfully, immediately
131+
/// followed by <see cref="SendLongPressing(View, GestureStatus, Func{IElement, Point?})"/> with
132+
/// <see cref="GestureStatus.Completed"/>. It must be called on the UI thread.</para>
133+
/// </remarks>
134+
/// <exception cref="ArgumentNullException"><paramref name="sender"/> is <see langword="null"/>.</exception>
135+
[EditorBrowsable(EditorBrowsableState.Never)]
136+
public void SendLongPressed(View sender, Func<IElement?, Point?>? getPosition = null)
126137
{
138+
_ = sender ?? throw new ArgumentNullException(nameof(sender));
139+
127140
var cmd = Command;
128141
if (cmd != null && cmd.CanExecute(CommandParameter))
129142
cmd.Execute(CommandParameter);
@@ -132,10 +145,24 @@ internal void SendLongPressed(View sender, Func<IElement?, Point?>? getPosition
132145
}
133146

134147
/// <summary>
135-
/// Sends the long pressing event with state information.
148+
/// Updates <see cref="State"/> and raises the <see cref="LongPressing"/> event.
136149
/// </summary>
137-
internal void SendLongPressing(View sender, GestureStatus status, Func<IElement?, Point?>? getPosition = null)
150+
/// <param name="sender">The view on which the long press gesture is being recognized.</param>
151+
/// <param name="status">The current status of the gesture.</param>
152+
/// <param name="getPosition">A function that returns the long press position relative to a specified element.</param>
153+
/// <remarks>
154+
/// <para>This infrastructure method is intended for gesture platform managers.</para>
155+
/// <para>Platform backends should raise <see cref="GestureStatus.Started"/> once the press is
156+
/// recognized, <see cref="GestureStatus.Running"/> while it is held, and finish with either
157+
/// <see cref="GestureStatus.Completed"/> or <see cref="GestureStatus.Canceled"/>. It must be
158+
/// called on the UI thread.</para>
159+
/// </remarks>
160+
/// <exception cref="ArgumentNullException"><paramref name="sender"/> is <see langword="null"/>.</exception>
161+
[EditorBrowsable(EditorBrowsableState.Never)]
162+
public void SendLongPressing(View sender, GestureStatus status, Func<IElement?, Point?>? getPosition = null)
138163
{
164+
_ = sender ?? throw new ArgumentNullException(nameof(sender));
165+
139166
State = status;
140167
LongPressing?.Invoke(sender, new LongPressingEventArgs(status, getPosition));
141168
}

src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
5252
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
5353
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
5454
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
55+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
56+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
5557
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
5658
Microsoft.Maui.Controls.LongPressedEventArgs
5759
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
3232
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
3333
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
3434
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
35+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
36+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
3537
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
3638
Microsoft.Maui.Controls.LongPressedEventArgs
3739
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
3232
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
3333
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
3434
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
35+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
36+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
3537
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
3638
Microsoft.Maui.Controls.LongPressedEventArgs
3739
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
4444
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
4545
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
4646
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
47+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
48+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
4749
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
4850
Microsoft.Maui.Controls.LongPressedEventArgs
4951
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
4040
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
4141
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
4242
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
43+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
44+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
4345
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
4446
Microsoft.Maui.Controls.LongPressedEventArgs
4547
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
6161
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
6262
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
6363
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
64+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
65+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
6466
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
6567
Microsoft.Maui.Controls.LongPressedEventArgs
6668
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.get -> i
5252
Microsoft.Maui.Controls.LongPressGestureRecognizer.MinimumPressDuration.set -> void
5353
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.get -> int
5454
Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouchesRequired.set -> void
55+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressed(Microsoft.Maui.Controls.View! sender, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
56+
Microsoft.Maui.Controls.LongPressGestureRecognizer.SendLongPressing(Microsoft.Maui.Controls.View! sender, Microsoft.Maui.GestureStatus status, System.Func<Microsoft.Maui.IElement?, Microsoft.Maui.Graphics.Point?>? getPosition = null) -> void
5557
Microsoft.Maui.Controls.LongPressGestureRecognizer.State.get -> Microsoft.Maui.GestureStatus
5658
Microsoft.Maui.Controls.LongPressedEventArgs
5759
Microsoft.Maui.Controls.LongPressedEventArgs.LongPressedEventArgs(object? parameter) -> void

src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<ProjectReference Include="..\..\..\Controls\Maps\src\Controls.Maps.csproj" />
2020
<ProjectReference Include="..\..\..\Controls\src\Core\Controls.Core.csproj" />
2121
<ProjectReference Include="..\..\..\Controls\src\Xaml\Controls.Xaml.csproj" />
22+
<ProjectReference Include="..\..\..\Controls\tests\ExternalGestureBackend\Controls.Tests.ExternalGestureBackend.csproj" />
2223
<ProjectReference Include="..\..\..\Core\maps\src\Maps.csproj" />
2324
<ProjectReference Include="..\..\..\Core\src\Core.csproj" />
2425
<ProjectReference Include="..\..\..\Essentials\src\Essentials.csproj" />

0 commit comments

Comments
 (0)