From 15218a06fe060a32749e607bb842838ebdc79e8f Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 9 Aug 2026 22:50:20 -0500 Subject: [PATCH] Start the fade-in only after the menu surface has been submitted The OS hit-tests a layered window against its last submitted surface, which lags Window.Show by several frames. Running the fade immediately meant the animation played against an invisible, click-through window: the menu popped in mid-fade at high opacity, and clicks in the gap fell through to the window beneath, dismissing the menu via deactivation. Deferring the fade to the second composition tick makes the menu hittable from the first visible pixel and makes the fade visible at all. Fixes #74 --- RadialActions/Services/MenuService.cs | 45 ++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/RadialActions/Services/MenuService.cs b/RadialActions/Services/MenuService.cs index fbc6aa6..01d2d28 100644 --- a/RadialActions/Services/MenuService.cs +++ b/RadialActions/Services/MenuService.cs @@ -1,5 +1,6 @@ using System.Windows; using System.Windows.Input; +using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; @@ -14,6 +15,7 @@ internal sealed class MenuService private readonly Dispatcher _dispatcher; private bool _isFadingOut; + private int _fadeInRequestVersion; public MenuService( Window window, @@ -51,7 +53,44 @@ public void ShowMenu(bool atCursor) _ = FocusMenuForKeyboardInputAsync(); _pieMenu.ResetInputState(); _window.IsHitTestVisible = true; - BeginFadeIn(); + BeginFadeInWhenSurfaceReady(); + } + + /// + /// Starts the fade-in only after the layered window has rendered and submitted a frame. + /// + /// + /// The OS hit-tests a layered window against its last submitted surface, and the first submission after + /// lags by several frames. Starting the animation immediately made the fade run + /// against a still-invisible, click-through window: the menu appeared mid-animation at high opacity, and + /// clicks in that gap fell through to the window beneath, which dismissed the menu via deactivation. + /// Waiting two composition ticks (one to render the shown surface, one so it is submitted) keeps the menu + /// hittable from the first visible pixel and lets the full fade actually be seen. + /// + private void BeginFadeInWhenSurfaceReady() + { + var version = ++_fadeInRequestVersion; + var renderedTicks = 0; + + void OnRendering(object sender, EventArgs e) + { + if (version != _fadeInRequestVersion) + { + CompositionTarget.Rendering -= OnRendering; + return; + } + + renderedTicks++; + if (renderedTicks < 2) + { + return; + } + + CompositionTarget.Rendering -= OnRendering; + BeginFadeIn(); + } + + CompositionTarget.Rendering += OnRendering; } public void HideMenu(bool animate = true) @@ -122,6 +161,8 @@ private void BeginFadeIn() private void BeginFadeOut() { + // Cancels any fade-in still waiting on its first rendered frame. + _fadeInRequestVersion++; StopFadeAnimations(); if (IsReducedMotionEnabled()) @@ -148,6 +189,8 @@ private static bool IsReducedMotionEnabled() private void HideMenuImmediately() { + // Cancels any fade-in still waiting on its first rendered frame. + _fadeInRequestVersion++; _isFadingOut = false; StopFadeAnimations(); _window.IsHitTestVisible = false;