From 804a1a67535bc5b170a371d483b7935644fc4a5f Mon Sep 17 00:00:00 2001 From: YoruDev-Ryland <159859604+YoruDev-Ryland@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:11:56 -0500 Subject: [PATCH] fix(app): menu marquee no longer crashes the app after its window closes DsFolderButton.Glide looped an overflowing label's animation in an async void until the button was deselected. Leaving the app with a menu entry scrolling disposed the window's services; the next TranslateToAsync threw ObjectDisposedException out of the async void and crashed PKForge (seen on an AYN Odin 3, reproduced on the emulator). The loop now ends when the label leaves the screen and treats a disposed window as the end of the animation. --- src/PKForge.App/Views/DsKit.cs | 48 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/PKForge.App/Views/DsKit.cs b/src/PKForge.App/Views/DsKit.cs index 7e2b8c9a..277c36e7 100644 --- a/src/PKForge.App/Views/DsKit.cs +++ b/src/PKForge.App/Views/DsKit.cs @@ -146,26 +146,36 @@ private async void StartMarquee() private async void Glide(Label line, int generation) { - // A line narrower than the button fits: leave it alone (measuring it outside the - // layout pass can shift it out of place). - if (line.Width < _labelViewport.Width - 4) return; - var measured = line.Measure(double.PositiveInfinity, HeightRequest).Width; - var overflow = measured - _labelViewport.Width; - if (overflow <= 4) return; - // At rest a detail line ends in "…"; while it scrolls it shows in full. - line.LineBreakMode = LineBreakMode.NoWrap; - line.WidthRequest = measured; - _gliding.Add(line); - - while (_selected && generation == _marqueeGeneration) + try { - var duration = (uint)Math.Clamp(overflow * 45, 1800, 6500); - await line.TranslateToAsync(-overflow, 0, duration, Easing.Linear); - if (!_selected || generation != _marqueeGeneration) return; - await Task.Delay(900); - if (!_selected || generation != _marqueeGeneration) return; - line.TranslationX = 0; - await Task.Delay(700); + // A line narrower than the button fits: leave it alone (measuring it outside the + // layout pass can shift it out of place). + if (line.Width < _labelViewport.Width - 4) return; + var measured = line.Measure(double.PositiveInfinity, HeightRequest).Width; + var overflow = measured - _labelViewport.Width; + if (overflow <= 4) return; + // At rest a detail line ends in "…"; while it scrolls it shows in full. + line.LineBreakMode = LineBreakMode.NoWrap; + line.WidthRequest = measured; + _gliding.Add(line); + + // The loop ends with the button: deselected, taken off screen (a menu closed) or gone + // with its window (the app was left), so it never animates a view nothing shows. + while (_selected && generation == _marqueeGeneration && line.Handler is not null) + { + var duration = (uint)Math.Clamp(overflow * 45, 1800, 6500); + await line.TranslateToAsync(-overflow, 0, duration, Easing.Linear); + if (!_selected || generation != _marqueeGeneration) return; + await Task.Delay(900); + if (!_selected || generation != _marqueeGeneration) return; + line.TranslationX = 0; + await Task.Delay(700); + } + } + catch (Exception error) when (error is ObjectDisposedException or InvalidOperationException) + { + // The window closed between two steps (its services are disposed): nothing is left to + // animate. Escaping an async void would take the whole app down instead. } }