diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 0bfc713ba0..397826df53 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -9,6 +9,16 @@ namespace Avalonia.Threading /// public class AvaloniaScheduler : LocalScheduler { + /// + /// Users can schedule actions on the dispatcher thread while being on the correct thread already. + /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. + /// To prevent this we are limiting amount of reentrant calls to before we will + /// schedule on a dispatcher anyway. + /// + private const int MaxReentrantSchedules = 32; + + private int _reentrancyGuard; + /// /// The instance of the . /// @@ -24,31 +34,58 @@ namespace Avalonia.Threading /// public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { - var composite = new CompositeDisposable(2); + IDisposable PostOnDispatcher() + { + var composite = new CompositeDisposable(2); + + var cancellation = new CancellationDisposable(); + + Dispatcher.UIThread.Post(() => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.DataBind); + + composite.Add(cancellation); + + return composite; + } + if (dueTime == TimeSpan.Zero) { if (!Dispatcher.UIThread.CheckAccess()) { - var cancellation = new CancellationDisposable(); - Dispatcher.UIThread.Post(() => - { - if (!cancellation.Token.IsCancellationRequested) - { - composite.Add(action(this, state)); - } - }, DispatcherPriority.DataBind); - composite.Add(cancellation); + return PostOnDispatcher(); } else { - return action(this, state); + if (_reentrancyGuard >= MaxReentrantSchedules) + { + return PostOnDispatcher(); + } + + try + { + _reentrancyGuard++; + + return action(this, state); + } + finally + { + _reentrancyGuard--; + } } } else { + var composite = new CompositeDisposable(2); + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + + return composite; } - return composite; } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index b458b15c64..cf0652247f 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -66,6 +66,7 @@ namespace Avalonia.Controls static ToolTip() { TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged); + IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged); IsOpenProperty.Changed.Subscribe(IsOpenChanged); } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 569697304f..e2a0f9e50c 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,20 +28,33 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; - control.DetachedFromVisualTree -= ControlDetaching; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; + } + } + + internal void TipOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + var control = (Control)e.Sender; + + if (e.OldValue is false && e.NewValue is true) + { control.DetachedFromVisualTree += ControlDetaching; } + else if(e.OldValue is true && e.NewValue is false) + { + control.DetachedFromVisualTree -= ControlDetaching; + } } private void ControlDetaching(object sender, VisualTreeAttachmentEventArgs e) { var control = (Control)sender; + control.DetachedFromVisualTree -= ControlDetaching; Close(control); } diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 8ee818bad2..0fd6b144a4 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -25,7 +25,7 @@ - + + IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.IsNullOrEmpty}}" + HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" + VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> + CaretBrush="{TemplateBinding CaretBrush}" + HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" + VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 9d7bc6af74..e52e7a487b 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; @@ -64,6 +65,39 @@ namespace Avalonia.Controls.UnitTests Assert.False(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + window.DataContext = new ToolTipViewModel(); + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + var target = window.Find("PART_target"); + var panel = window.Find("PART_panel"); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + + panel.Children.Remove(target); + + Assert.False(ToolTip.GetIsOpen(target)); + } + } [Fact] public void Should_Open_On_Pointer_Enter() @@ -208,4 +242,9 @@ namespace Avalonia.Controls.UnitTests } } } + + internal class ToolTipViewModel + { + public string Tip => "Tip"; + } }