Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore pointer capture for PointerEntered and PointerExited events following UWP behavior #15357

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Avalonia.Base/Input/PointerOverPreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public void OnNext(RawInputEventArgs value)
else if (pointerDevice.TryGetPointer(args) is { } pointer &&
pointer.Type != PointerType.Touch)
{
var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor;
// See PointerOver_Or_Exited_Should_Ignore_Capture test.
// Following UWP behavior, PointerEntered and PointerExited events should always ignore captured element.
var element = args.InputHitTestResult.firstEnabledAncestor;

SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position,
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
Expand All @@ -84,7 +86,7 @@ public void SceneInvalidated(Rect dirtyRect)

if (dirtyRect.Contains(clientPoint))
{
var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint);
var element = _inputRoot.InputHitTest(clientPoint);
SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint))
Expand Down
8 changes: 6 additions & 2 deletions src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,28 @@ namespace Avalonia.Diagnostics.ViewModels
internal class FiredEvent : ViewModelBase
{
private readonly RoutedEventArgs _eventArgs;
private readonly RoutedEvent? _originalEvent;
private EventChainLink? _handledBy;

public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator, DateTime triggerTime)
{
_eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs));
Originator = originator ?? throw new ArgumentNullException(nameof(originator));
_originalEvent = _eventArgs.RoutedEvent;
AddToChain(originator);
TriggerTime = triggerTime;
}

public bool IsPartOfSameEventChain(RoutedEventArgs e)
{
return e == _eventArgs;
// Note, Avalonia might reuse RoutedEventArgs for different events to avoid extra allocations.
// Like, PointerEntered and PointerExited will use the same instance of RoutedEventArgs.
return e == _eventArgs && e.RoutedEvent == _originalEvent;
}

public DateTime TriggerTime { get; }

public RoutedEvent Event => _eventArgs.RoutedEvent!;
public RoutedEvent Event => _originalEvent!;

public bool IsHandled => HandledBy?.Handled == true;

Expand Down
97 changes: 54 additions & 43 deletions tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,42 +116,6 @@ public void TouchMove_Should_Not_Set_IsPointerOver()
Assert.False(root.IsPointerOver);
}

[Fact]
public void HitTest_Should_Be_Ignored_If_Element_Captured()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));

var renderer = new Mock<IHitTester>();
var pointer = new Mock<IPointer>();
var device = CreatePointerDeviceMock(pointer.Object).Object;
var impl = CreateTopLevelImplMock();

Canvas canvas;
Border border;
Decorator decorator;

var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
}, renderer.Object);

SetHit(renderer, canvas);
pointer.SetupGet(p => p.Captured).Returns(decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));

Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}

[Fact]
public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
{
Expand Down Expand Up @@ -492,16 +456,63 @@ void HandleEvent(object? sender, PointerEventArgs e)
result);
}

private static void AddEnteredExitedHandlers(
EventHandler<PointerEventArgs> handler,
params IInputElement[] controls)
// https://github.com/AvaloniaUI/Avalonia/issues/15293
[Fact]
public void PointerOver_Or_Exited_Should_Ignore_Capture()
{
foreach (var c in controls)
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));

var renderer = new Mock<IHitTester>();
var device = new MouseDevice();
var impl = CreateTopLevelImplMock();

var result = new List<(object, string, object?, bool)>();

Canvas canvas;

var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
}, renderer.Object);

void HandleEvent(object? sender, PointerEventArgs e)
{
c.PointerEntered += handler;
c.PointerExited += handler;
c.PointerMoved += handler;
result.Add((sender!, e.RoutedEvent!.Name, e.Pointer.Captured, canvas.IsPointerOver));
}

AddEnteredExitedHandlers(HandleEvent, canvas);
AddPressedReleasedHandlers(HandleEvent, canvas);

// Init pointer over.
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move));

// Init capture.
impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown));

// Leave the control, still captured.
impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move));
SetHit(renderer, null);
impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move));

// Release capture.
impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp));

Assert.Equal(
new[]
{
((object)canvas, nameof(InputElement.PointerEntered), (object?)null, true),
(canvas, nameof(InputElement.PointerMoved), null, true),
(canvas, nameof(InputElement.PointerPressed), canvas, true),
(canvas, nameof(InputElement.PointerMoved), canvas, true),
(canvas, nameof(InputElement.PointerExited), canvas, false),
(canvas, nameof(InputElement.PointerMoved), canvas, false),
(canvas, nameof(InputElement.PointerReleased), canvas, false)
},
result);
}
}
}
23 changes: 23 additions & 0 deletions tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,27 @@ private protected static TopLevel CreateInputRoot(IWindowImpl impl, Control chil

return pointerDevice;
}

protected static void AddEnteredExitedHandlers(
EventHandler<PointerEventArgs> handler,
params IInputElement[] controls)
{
foreach (var c in controls)
{
c.PointerEntered += handler;
c.PointerExited += handler;
c.PointerMoved += handler;
}
}

protected static void AddPressedReleasedHandlers(
EventHandler<PointerEventArgs> handler,
params IInputElement[] controls)
{
foreach (var c in controls)
{
c.PointerPressed += (s, a) => handler(s, a);
c.PointerReleased += (s, a) => handler(s, a);
}
}
}