Repository: AvaloniaUI/Avalonia

Issue:
The ToolTip Popup not close when pointer exit the control,Is that a feature or bug?
I have a new problem when  set 'OverlayPopups =true',the ToolTip Popup not close when pointer exit the control,Is that a feature or bug?
![tooltip](https://github.com/user-attachments/assets/5035f341-8a81-4c46-99b6-ddcb89e03f21)
@maxkatz6

_Originally posted by @ZSYMAX in https://github.com/AvaloniaUI/Avalonia/discussions/15212#discussioncomment-10175448_


Hints:

Patch:
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index d01114be6fb..d99d185bedc 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -78,7 +78,7 @@ public void Update(IInputRoot root, Visual? candidateToolTipHost)
         {
             var currentToolTip = _tipControl?.GetValue(ToolTip.ToolTipProperty);
 
-            if (root == currentToolTip?.VisualRoot)
+            if (root == currentToolTip?.PopupHost?.HostedVisualTreeRoot)
             {
                 // Don't update while the pointer is over a tooltip
                 return;


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index 6199a208720..12ecbb0c68f 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -1,9 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.Reactive;
 using System.Runtime.CompilerServices;
+using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Threading;
 using Avalonia.UnitTests;
@@ -15,18 +18,65 @@ namespace Avalonia.Controls.UnitTests
     public class ToolTipTests_Popup : ToolTipTests
     {
         protected override TestServices ConfigureServices(TestServices baseServices) => baseServices;
+
+        protected override void SetupWindowMock(Mock<IWindowImpl> windowImpl) { }
+
+        protected override void VerifyToolTipType(Control control)
+        {
+            var toolTip = control.GetValue(ToolTip.ToolTipProperty);
+            Assert.IsType<PopupRoot>(toolTip.PopupHost);
+            Assert.Same(toolTip.VisualRoot, toolTip.PopupHost);
+        }
     }
 
-    public class ToolTipTests_Overlay : ToolTipTests
+    public class ToolTipTests_Overlay : ToolTipTests, IDisposable
     {
+        private readonly IDisposable _toolTipOpenSubscription;
+
+        public ToolTipTests_Overlay()
+        {
+            _toolTipOpenSubscription = ToolTip.IsOpenProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<bool>>(e =>
+            {
+                if (e.Sender is Visual { VisualRoot: {} root } visual)
+                    OverlayLayer.GetOverlayLayer(visual).Measure(root.ClientSize);
+            }));
+        }
+
+        public void Dispose()
+        {
+            _toolTipOpenSubscription.Dispose();
+        }
+
         protected override TestServices ConfigureServices(TestServices baseServices) =>
             baseServices.With(windowingPlatform: new MockWindowingPlatform(popupImpl: window => null));
+
+        protected override void SetupWindowMock(Mock<IWindowImpl> windowImpl)
+        {
+            windowImpl.Setup(x => x.CreatePopup()).Returns(default(IPopupImpl));
+        }
+
+        protected override void VerifyToolTipType(Control control)
+        {
+            var toolTip = control.GetValue(ToolTip.ToolTipProperty);
+            Assert.IsType<OverlayPopupHost>(toolTip.PopupHost);
+            Assert.Same(toolTip.VisualRoot, control.VisualRoot);
+        }
     }
 
     public abstract class ToolTipTests
     {
         protected abstract TestServices ConfigureServices(TestServices baseServices);
 
+        protected abstract void SetupWindowMock(Mock<IWindowImpl> windowImpl);
+
+        protected abstract void VerifyToolTipType(Control control);
+
+        private void AssertToolTipOpen(Control control)
+        {
+            Assert.True(ToolTip.GetIsOpen(control));
+            VerifyToolTipType(control);
+        }
+
         private static readonly MouseDevice s_mouseDevice = new(new Pointer(0, PointerType.Mouse, true));
 
         [Fact]
@@ -46,7 +96,7 @@ public void Should_Close_When_Control_Detaches()
 
                 SetupWindowAndActivateToolTip(panel, target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 panel.Children.Remove(target);
 
@@ -74,7 +124,7 @@ public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree()
 
                 mouseEnter(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 panel.Children.Remove(target);
 
@@ -95,7 +145,7 @@ public void Should_Open_On_Pointer_Enter()
 
                 SetupWindowAndActivateToolTip(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
             }
         }
 
@@ -112,7 +162,7 @@ public void Content_Should_Update_When_Tip_Property_Changes_And_Already_Open()
 
                 SetupWindowAndActivateToolTip(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
                 Assert.Equal("Tip", target.GetValue(ToolTip.ToolTipProperty).Content);
 
                 ToolTip.SetTip(target, "Tip1");
@@ -139,7 +189,7 @@ public void Should_Open_On_Pointer_Enter_With_Delay()
 
                 timer.ForceFire();
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
             }
         }
 
@@ -188,6 +238,7 @@ public void Setting_IsOpen_Should_Add_Open_Class()
                 ToolTip.SetIsOpen(decorator, true);
 
                 Assert.Equal(new[] { ":open" }, toolTip.Classes);
+                VerifyToolTipType(decorator);
             }
         }
 
@@ -197,8 +248,11 @@ public void Clearing_IsOpen_Should_Remove_Open_Class()
             using (UnitTestApplication.Start(ConfigureServices(TestServices.StyledWindow)))
             {
                 var toolTip = new ToolTip();
-                var window = new Window();
 
+                var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                SetupWindowMock(windowImpl);
+                var window = new Window(windowImpl.Object);
+    
                 var decorator = new Decorator()
                 {
                     [ToolTip.TipProperty] = toolTip
@@ -211,6 +265,7 @@ public void Clearing_IsOpen_Should_Remove_Open_Class()
                 window.Presenter.ApplyTemplate();
 
                 ToolTip.SetIsOpen(decorator, true);
+                AssertToolTipOpen(decorator);
                 ToolTip.SetIsOpen(decorator, false);
 
                 Assert.Empty(toolTip.Classes);
@@ -230,7 +285,7 @@ public void Should_Close_On_Null_Tip()
 
                 SetupWindowAndActivateToolTip(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 target[ToolTip.TipProperty] = null;
 
@@ -253,13 +308,13 @@ public void Should_Not_Close_When_Pointer_Is_Moved_Over_ToolTip()
 
                 mouseEnter(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
 
                 mouseEnter(tooltip);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
             }
         }
 
@@ -277,16 +332,16 @@ public void Should_Not_Close_When_Pointer_Is_Moved_From_ToolTip_To_Original_Cont
                 var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
 
                 mouseEnter(target);
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
                 mouseEnter(tooltip);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 mouseEnter(target);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
             }
         }
 
@@ -311,12 +366,12 @@ public void Should_Close_When_Pointer_Is_Moved_From_ToolTip_To_Another_Control()
                 var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
 
                 mouseEnter(target);
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
                 mouseEnter(tooltip);
 
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 mouseEnter(other);
 
@@ -352,7 +407,7 @@ public void New_ToolTip_Replaces_Other_ToolTip_Immediately()
             Assert.False(ToolTip.GetIsOpen(other)); // long delay
 
             mouseEnter(target);
-            Assert.True(ToolTip.GetIsOpen(target)); // no delay
+            AssertToolTipOpen(target); // no delay
 
             mouseEnter(other);
             Assert.True(ToolTip.GetIsOpen(other)); // delay skipped, a tooltip was already open
@@ -360,7 +415,7 @@ public void New_ToolTip_Replaces_Other_ToolTip_Immediately()
             // Now disable the between-show system
 
             mouseEnter(target);
-            Assert.True(ToolTip.GetIsOpen(target));
+            AssertToolTipOpen(target);
 
             ToolTip.SetBetweenShowDelay(other, -1);
 
@@ -389,7 +444,7 @@ public void ToolTip_Events_Order_Is_Defined()
 
             SetupWindowAndActivateToolTip(target);
 
-            Assert.True(ToolTip.GetIsOpen(target));
+            AssertToolTipOpen(target);
 
             target[ToolTip.TipProperty] = null;
 
@@ -442,7 +497,7 @@ public void ToolTip_Can_Be_Replaced_On_The_Fly_Via_Opening_Event()
 
             SetupWindowAndActivateToolTip(target);
 
-            Assert.True(ToolTip.GetIsOpen(target));
+            AssertToolTipOpen(target);
 
             target[ToolTip.TipProperty] = null;
 
@@ -463,7 +518,7 @@ public void Should_Close_When_Pointer_Leaves_Window()
                 var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
 
                 mouseEnter(target);
-                Assert.True(ToolTip.GetIsOpen(target));
+                AssertToolTipOpen(target);
 
                 var topLevel = TopLevel.GetTopLevel(target);
                 topLevel.PlatformImpl.Input(new RawPointerEventArgs(s_mouseDevice, (ulong)DateTime.Now.Ticks, topLevel, 
@@ -476,6 +531,8 @@ public void Should_Close_When_Pointer_Leaves_Window()
         private Action<Control> SetupWindowAndGetMouseEnterAction(Control windowContent, [CallerMemberName] string testName = null)
         {
             var windowImpl = MockWindowingPlatform.CreateWindowMock();
+            SetupWindowMock(windowImpl);
+
             var hitTesterMock = new Mock<IHitTester>();
 
             var window = new Window(windowImpl.Object)