Skip to content

Commit

Permalink
Never update ToolTipService while the mouse is over a tooltip (fixes …
Browse files Browse the repository at this point in the history
…flicker when redrawing a window beneath the pointer) (#15596)

Avoid race condition where a dispatcher timer callback exectues right after we stopped the timer
Fix not closing a tooltip when its pointer exit event is the last input sent to Avalonia
  • Loading branch information
TomEdwardsEnscape authored May 6, 2024
1 parent fd78546 commit 76f4e7a
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 12 deletions.
5 changes: 3 additions & 2 deletions src/Avalonia.Controls/IToolTipService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using Avalonia.Metadata;
using Avalonia.Input;
using Avalonia.Metadata;

namespace Avalonia.Controls;

[Unstable, PrivateApi]
internal interface IToolTipService
{
void Update(Visual? candidateToolTipHost);
void Update(IInputRoot root, Visual? candidateToolTipHost);
}
32 changes: 24 additions & 8 deletions src/Avalonia.Controls/ToolTipService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class ToolTipService : IToolTipService, IDisposable
private long _lastTipCloseTime;
private DispatcherTimer? _timer;
private ulong _lastTipEventTime;
private ulong _lastWindowEventTime;

public ToolTipService(IInputManager inputManager)
{
Expand All @@ -36,18 +37,23 @@ private void InputManager_OnProcess(RawInputEventArgs e)
{
if (e is RawPointerEventArgs pointerEvent)
{
bool isTooltipEvent = false;
if (_tipControl?.GetValue(ToolTip.ToolTipProperty) is { } currentTip && e.Root == currentTip.PopupHost)
{
isTooltipEvent = true;
_lastTipEventTime = pointerEvent.Timestamp;

var simultaneousTipEvent = _lastTipEventTime == pointerEvent.Timestamp;
}
else if (e.Root == _tipControl?.VisualRoot)
{
_lastWindowEventTime = pointerEvent.Timestamp;
}

switch (pointerEvent.Type)
{
// sometimes there is a null hit test as soon as the pointer enters a tooltip
case RawPointerEventType.Move when !(simultaneousTipEvent && pointerEvent.InputHitTestResult.element == null):
Update(pointerEvent.InputHitTestResult.element as Visual);
case RawPointerEventType.Move:
Update(pointerEvent.Root, pointerEvent.InputHitTestResult.element as Visual);
break;
case RawPointerEventType.LeaveWindow when e.Root == _tipControl?.VisualRoot && !simultaneousTipEvent:
case RawPointerEventType.LeaveWindow when (e.Root == _tipControl?.VisualRoot && _lastTipEventTime != e.Timestamp) || (isTooltipEvent && _lastWindowEventTime != e.Timestamp):
ClearTip();
_tipControl = null;
break;
Expand All @@ -68,10 +74,16 @@ void ClearTip()
}
}

public void Update(Visual? candidateToolTipHost)
public void Update(IInputRoot root, Visual? candidateToolTipHost)
{
var currentToolTip = _tipControl?.GetValue(ToolTip.ToolTipProperty);

if (root == currentToolTip?.VisualRoot)
{
// Don't update while the pointer is over a tooltip
return;
}

while (candidateToolTipHost != null)
{
if (candidateToolTipHost == currentToolTip) // when OverlayPopupHost is in use, the tooltip is in the same window as the host control
Expand Down Expand Up @@ -193,7 +205,11 @@ private void ToolTipPointerExited(object? sender, PointerEventArgs e)
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay), Tag = (this, control) };
_timer.Tick += (o, e) => Open(control);
_timer.Tick += (o, e) =>
{
if (_timer != null)
Open(control);
};
_timer.Start();
}

Expand Down
4 changes: 2 additions & 2 deletions src/Avalonia.Controls/TopLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -896,12 +896,12 @@ private static void OnToolTipServiceEnabledChanged(AvaloniaPropertyChangedEventA

private void UpdateToolTip(Rect dirtyRect)
{
if (_tooltipService != null && _pointerOverPreProcessor?.LastPosition is { } lastPos)
if (_tooltipService != null && IsPointerOver && _pointerOverPreProcessor?.LastPosition is { } lastPos)
{
var clientPoint = this.PointToClient(lastPos);
if (dirtyRect.Contains(clientPoint))
{
_tooltipService.Update(HitTester.HitTestFirst(clientPoint, this, null));
_tooltipService.Update(this, HitTester.HitTestFirst(clientPoint, this, null));
}
}
}
Expand Down

0 comments on commit 76f4e7a

Please sign in to comment.