From c8da7f891edbb25a303edb77948735967f3ee9be Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Tue, 21 Jul 2026 17:49:59 +1000 Subject: [PATCH] fix: detach platform input when presentation source closes (#21782) * test: cover platform input detachment on close Closed top levels retain the presentation source as the platform input callback. A late platform input event can therefore target an already disposed visual tree.\n\nThe regression test requires top-level closure to clear ITopLevelImpl.Input. * fix: detach platform input when presentation source closes Win32 can deliver WM_CAPTURECHANGED while destroying a popup that owns mouse capture. The platform then calls the input handler retained by the disposed presentation source, which logs a warning and can route input into a detached visual tree.\n\nClear the platform input callback at the start of presentation source disposal, alongside the existing scaling event unsubscription. --------- Co-authored-by: Julien Lebosquain --- .../PresentationSource/PresentationSource.cs | 7 ++++++- .../PresentationSourceTests.cs | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index c2d5516667..60ec963e6f 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -77,6 +77,12 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi public void Dispose() { + if (PlatformImpl is { } platformImpl) + { + platformImpl.Input = null; + platformImpl.ScalingChanged -= HandleScalingChanged; + } + _layoutDiagnosticBridge?.Dispose(); _layoutDiagnosticBridge = null; LayoutManager.Dispose(); @@ -84,7 +90,6 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); - PlatformImpl?.ScalingChanged -= HandleScalingChanged; PlatformImpl = null; _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); diff --git a/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs b/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs index 97892e9b4c..9a8172d3cf 100644 --- a/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PresentationSourceTests.cs @@ -19,6 +19,20 @@ namespace Avalonia.Controls.UnitTests; public sealed class PresentationSourceTests : ScopedTestBase { + [Fact] + public void Closing_Should_Detach_Platform_Input_Handler() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + var window = new Window(windowImpl.Object); + + Assert.NotNull(windowImpl.Object.Input); + + windowImpl.Object.Closed!(); + + Assert.Null(windowImpl.Object.Input); + } + [Fact] public void ChromeHitTest_Prefers_Overlay_Over_Content() {