mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
[Quick Accent] Cloak the accent bar instead of hiding it (#49655)
## Summary of the Pull Request Follow-up to #49633: replace the way the accent bar's first frame is protected. #49633 fixed the blank/stale first frame (#49489) by masking it — `Selector.Opacity = 0`, unveil after two `CompositionTarget.Rendering` ticks, backed by a 150 ms watchdog. This PR removes the cause instead, using the technique the Command Palette and Quick Access already ship: **DWM-cloak the overlay instead of hiding it**, so it never stops rendering and there is no stale frame to put back on screen. No user-visible behaviour change is intended beyond removing the fixed two-render-tick reveal delay; this is a mechanism swap plus the cleanup it enables. ## PR Checklist - [x] **Closes:** N/A — #49489 was already closed by #49633; this replaces that fix's mechanism - [x] **Communication:** follow-up to a merged PR in the same module, no new feature surface - [x] **Tests:** `PowerAccent.Core.UnitTests` 32/32 still pass (the pure width logic from #49633 is untouched). The current-head CI status is tracked in the PR checks; the compositor-specific manual measurements below were captured before the final commit-fence follow-up and are labeled accordingly - [x] **Localization:** no new end-user-facing strings - [x] **Dev docs:** N/A - [x] **New binaries:** none - [x] **Documentation updated:** N/A ## Detailed Description of the Pull Request / Additional comments ### Why cloaking A hidden WinUI 3 window renders nothing. Its composition surface therefore still holds the frame it was showing when it was hidden, and `ShowWindow` puts that stale frame back on screen before the rebuilt accent list has been laid out — that is #49489. Everything downstream of that follows from "the window does not render while hidden": * the bar cannot be measured before it is shown (a `Collapsed` subtree is never measured), hence #49633's measure-twice workaround; * #49633 deferred reveal using rendering ticks because it did not have a composition-commit fence; * this PR calls `Microsoft.UI.Composition.Compositor.RequestCommitAsync()` after the bar has been laid out, sized, positioned and scrolled, and only reveals after that commit completes. This replaces the frame counter and watchdog with an explicit compositor fence. A cloaked window is equally invisible to the user but stays `SW_SHOWNA`-shown, so XAML keeps laying it out and painting it. This is exactly what `Microsoft.CmdPal.UI\MainWindow.xaml.cs` does, and its comment names the same symptom: ```csharp // TRICKY: show our HWND again. This will trick XAML into painting our // HWND again, so that we avoid the "flicker" caused by a WinUI3 app // window being first shown ``` `QuickAccess.UI\QuickAccessXAML\MainWindow.xaml.cs` uses the same pattern, including the "warm up the window while cloaked" prewarm that this PR also picks up — which is what removes the *first summon of the process* case that #49633's second measurement existed for. ### What the summon looks like now `Show()` still raises `Showing`, so the surface leaves `Collapsed` and the bar lays out — but the window is still cloaked, so nothing reaches the screen. The bar is then measured **once** (on a templated, laid-out, non-collapsed subtree), sized, positioned and scrolled to the selection. The compositor commit is then awaited, and only after it completes does `Reveal()` uncloak the window. The first visible frame is a finished bar by construction rather than by timing. Removed as a result: `RevealTimeoutMs`, `FramesBeforeReveal`, `_revealTimer`, `_revealGeneration`, `_renderedFrames`, `_measuredContentWidthDip`, `ArmRevealTimeout`, `CancelPendingReveal`, `WaitForFirstFrameThenReveal`, `OnRenderingBeforeReveal`, the local `Reveal`, and the `Selector.Opacity` dance — 87 net lines out of `MainWindow`. `_showGeneration` stays: a layout callback queued by a dismissed summon still has to be dropped. ### `TransparentWindow` The cloak lives in the shared window because `Hide()` owns the `AppWindow.Hide()` that has to be replaced. It is **opt-in** (`EnableCloakedHide()`), so Shortcut Guide's overlay and CmdPal's toast keep hiding exactly as they do today; only Quick Accent enables it. `Reveal()` is a no-op for them. Two details worth review attention: * **Hit-testing.** Cloaking takes a window out of composition but *not* out of hit-testing, and this HWND sits exactly where the user is typing. While cloaked the window is therefore made click-through (`WS_EX_TRANSPARENT`), restored on reveal. Without this, an invisible accent bar would swallow clicks meant for the app underneath. * **`SW_HIDE` then `SW_SHOWNA`.** Same order as CmdPal: the hide is what hands the foreground back to whatever window should own it, and the show that follows leaves the window "shown" — which is what keeps XAML painting — while the cloak keeps it off screen. If DWM refuses to cloak, the HWND remains hidden; a later `Show()` retries instead of exposing an un-laid-out frame. ### Relationship to #34849 / #41044 Always-on-top is still released on hide, so the dormant overlay is `WS_EX_TOPMOST=False` exactly as before — verified below. Cloaking is orthogonal to topmost. The one honest trade-off is that the HWND is now permanently `WS_VISIBLE` (cloaked), so it keeps participating in composition while dormant, the same as CmdPal and Quick Access already do; it stays out of Alt-Tab and the taskbar via a hidden owner plus `WS_EX_TOOLWINDOW`. ## Validation Steps Performed > The build/test/live-state results below were recorded at `e372cdf`. Current head `ea658bb` adds the explicit `RequestCommitAsync` fence after that validation. Current-head CI is tracked by the PR checks, and the live window-state/frame-capture checks should be repeated before merge. * `build-essentials`, `Common.UI.Controls`, `PowerAccent.UI` and `PowerAccent.Core.UnitTests` all build clean (Debug|x64), 0 warnings. * `PowerAccent.Core.UnitTests`: 32/32 pass. * Live window-state measurement against the built `PowerToys.PowerAccent.exe` (`DwmGetWindowAttribute(DWMWA_CLOAKED)` + `IsWindowVisible` + ex-styles), driving a real summon of the <kbd>R</kbd> bar with **All languages** selected: | phase | state | |---|---| | dormant (prewarmed, before any summon) | `visible=True cloaked=1 topmost=False clickThrough=True` | | summoned | `visible=True cloaked=0 topmost=True clickThrough=False` | | dismissed | `visible=True cloaked=1 topmost=False clickThrough=True` | i.e. the window is shown-and-painting the whole time, invisible and click-through while dormant, and topmost/interactive only while summoned. * Screen-captured the summoned bar: all 22 characters for <kbd>R</kbd> render (including the wide `₹ ៛ ﷼`), leading and trailing padding are symmetric, nothing is clipped and the selection is on the first cell. --------- Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -56,16 +56,20 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaNcRenderingPolicy = 2;
|
||||
private const int DwmwaCloak = 13;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
private const int DwmncrpDisabled = 2;
|
||||
|
||||
private const int GwlpHwndParent = -8;
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExDlgModalFrame = 0x00000001;
|
||||
private const int WsExTransparent = 0x00000020;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
private const int WsExWindowEdge = 0x00000100;
|
||||
private const int WsExClientEdge = 0x00000200;
|
||||
private const int WsExAppWindow = 0x00040000;
|
||||
|
||||
private const uint SwpNoSize = 0x0001;
|
||||
private const uint SwpNoMove = 0x0002;
|
||||
@@ -73,12 +77,16 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
private const uint SwpNoActivate = 0x0010;
|
||||
private const uint SwpFrameChanged = 0x0020;
|
||||
|
||||
private const int SwHide = 0;
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly nint _hwnd;
|
||||
|
||||
private Microsoft.UI.Xaml.Window? _hiddenOwnerWindow;
|
||||
private bool _inputHooked;
|
||||
private bool _seenActivated;
|
||||
private bool _cloakWhenHidden;
|
||||
private bool _cloaked;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
@@ -211,7 +219,9 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> without a transition, so subscribed content animates
|
||||
/// in using its own configured show transition.
|
||||
/// in using its own configured show transition. After
|
||||
/// <see cref="EnableCloakedHide"/> the window stays cloaked here and only
|
||||
/// becomes visible on <see cref="Reveal"/>.
|
||||
/// </summary>
|
||||
public void Show() => RaiseShow(null);
|
||||
|
||||
@@ -219,27 +229,50 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> so subscribed content animates in using
|
||||
/// <paramref name="transition"/>, overriding its configured show transition.
|
||||
/// After <see cref="EnableCloakedHide"/> the window stays cloaked here and only
|
||||
/// becomes visible on <see cref="Reveal"/>.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition the content should play.</param>
|
||||
public void Show(Transition transition) => RaiseShow(transition);
|
||||
|
||||
private void RaiseShow(Transition? transition)
|
||||
{
|
||||
// A new show can interrupt a deferred hide. In that case HideCore never runs, so the
|
||||
// previous Reveal left the HWND uncloaked. Cloak synchronously before the caller returns to
|
||||
// the dispatcher; otherwise content rebuilt for this summon can render at the old bounds.
|
||||
if (DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
// CloakAndKeepShown uses SW_HIDE, which can raise Deactivated. Reset this first so an
|
||||
// internal show transition is not mistaken for a user-initiated focus loss.
|
||||
_seenActivated = false;
|
||||
EnsureCloakedBeforeShow();
|
||||
}
|
||||
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_seenActivated = false;
|
||||
|
||||
// Also cover callers that entered Show from another thread.
|
||||
EnsureCloakedBeforeShow();
|
||||
EnsureInputHooks();
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
|
||||
// Cloaked mode is made SW_SHOWNA-visible only after cloaking succeeds.
|
||||
if (!_cloakWhenHidden)
|
||||
{
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
}
|
||||
|
||||
Showing?.Invoke(this, new ShowingEventArgs(transition));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="Hiding"/> so subscribed content animates out, then hides
|
||||
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> once every
|
||||
/// deferral taken by a handler has completed (immediately if none were taken).
|
||||
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> - or cloaks the
|
||||
/// window when <see cref="EnableCloakedHide"/> was called - once every deferral
|
||||
/// taken by a handler has completed (immediately if none were taken).
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
@@ -249,10 +282,143 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
var args = new HidingEventArgs();
|
||||
Hiding?.Invoke(this, args);
|
||||
args.RunWhenComplete(AppWindow.Hide);
|
||||
args.RunWhenComplete(HideCore);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches this window from hiding to <b>cloaking</b>, and immediately puts it
|
||||
/// into that state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>A hidden WinUI 3 window renders nothing, so its composition surface keeps
|
||||
/// whatever frame it was showing when it was hidden, and the next <see cref="Show()"/>
|
||||
/// puts that stale frame back on screen before the new content has been laid out.
|
||||
/// A cloaked window is equally invisible but stays <c>SW_SHOWNA</c>-shown, so XAML
|
||||
/// keeps laying it out and painting it and there is no stale frame to put back.</para>
|
||||
/// <para>This changes what the show sequence means: <see cref="Show()"/> still raises
|
||||
/// <see cref="Showing"/> so the content lays out and animates in, but the window stays
|
||||
/// cloaked - <see cref="Reveal"/> is what puts it on screen. A consumer that rebuilds
|
||||
/// its content on every summon can therefore lay that content out while still invisible
|
||||
/// and reveal a window that is correct in its first visible frame.</para>
|
||||
/// <para>Enabling it also warms the window up: the XAML tree is built, templated and
|
||||
/// painted right away rather than on the first summon.</para>
|
||||
/// <para>Call this once, from the consumer's constructor after its content has been
|
||||
/// set. Cloaking is a DWM feature; if DWM refuses, the window remains hidden and the
|
||||
/// next <see cref="Show()"/> retries.</para>
|
||||
/// </remarks>
|
||||
protected void EnableCloakedHide()
|
||||
{
|
||||
// Unlike a hidden HWND, a cloaked HWND remains WS_VISIBLE. Give it a hidden owner before
|
||||
// the first SW_SHOWNA so Explorer reliably keeps it out of the taskbar on every virtual-
|
||||
// desktop taskbar configuration; WS_EX_TOOLWINDOW alone is not sufficient there.
|
||||
EnsureHiddenOwner();
|
||||
_cloakWhenHidden = true;
|
||||
HideCore();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts a cloaked window on screen. Consumers call this once the content that
|
||||
/// <see cref="Show()"/> laid out is ready to be seen. Does nothing unless
|
||||
/// <see cref="EnableCloakedHide"/> was called and the window is currently cloaked.
|
||||
/// </summary>
|
||||
public void Reveal()
|
||||
{
|
||||
if (!_cloaked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the state and click-through style when DWM refuses to uncloak. A later Reveal can
|
||||
// then retry instead of returning early while the HWND is still invisible.
|
||||
if (!SetCloak(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cloaked = false;
|
||||
|
||||
// Restore hit-testing: the window is on screen again, so it must behave like any
|
||||
// other window (see CloakAndKeepShown for why it is click-through while cloaked).
|
||||
ApplyExStyleBit(WsExTransparent, false);
|
||||
}
|
||||
|
||||
private void HideCore()
|
||||
{
|
||||
if (_cloakWhenHidden)
|
||||
{
|
||||
CloakAndKeepShown();
|
||||
return;
|
||||
}
|
||||
|
||||
AppWindow.Hide();
|
||||
}
|
||||
|
||||
private void EnsureCloakedBeforeShow()
|
||||
{
|
||||
if (_cloakWhenHidden && !_cloaked)
|
||||
{
|
||||
CloakAndKeepShown();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureHiddenOwner()
|
||||
{
|
||||
if (_hiddenOwnerWindow is not null || _hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hiddenOwnerWindow = new Microsoft.UI.Xaml.Window();
|
||||
nint hiddenOwnerHwnd = WinRT.Interop.WindowNative.GetWindowHandle(_hiddenOwnerWindow);
|
||||
_ = SetWindowLongPtr(_hwnd, GwlpHwndParent, hiddenOwnerHwnd);
|
||||
|
||||
// WS_EX_APPWINDOW overrides normal owner-based taskbar suppression.
|
||||
ApplyExStyleBit(WsExAppWindow, false);
|
||||
}
|
||||
|
||||
private void CloakAndKeepShown()
|
||||
{
|
||||
if (_cloaked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide first so a DWM failure cannot leave an uncloaked overlay on screen. The next Show
|
||||
// retries cloaking; only a successfully cloaked HWND is made SW_SHOWNA-visible again.
|
||||
_ = ShowWindow(_hwnd, SwHide);
|
||||
if (!SetCloak(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cloaked = true;
|
||||
|
||||
// Cloaking only takes the window out of composition, not out of hit-testing, and
|
||||
// this HWND sits exactly where the user is working. Make it click-through so the
|
||||
// invisible window cannot swallow input meant for the app underneath it.
|
||||
ApplyExStyleBit(WsExTransparent, true);
|
||||
|
||||
// SW_HIDE above hands the foreground back to whatever window should own it (only the OS
|
||||
// can pick the right one). Now that cloaking succeeded, SW_SHOWNA leaves this window
|
||||
// "shown", which keeps XAML painting it, while the cloak keeps it off screen.
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
}
|
||||
|
||||
private bool SetCloak(bool cloak)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
int value = cloak ? 1 : 0;
|
||||
return DwmSetWindowAttribute(_hwnd, DwmwaCloak, &value, sizeof(int)) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActivatedForDismiss(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
|
||||
@@ -33,8 +33,8 @@ public partial class App : Application, IDisposable
|
||||
DispatcherQueueForApp = DispatcherQueue.GetForCurrentThread();
|
||||
Window = new MainWindow();
|
||||
|
||||
// Quick Accent has no visible main window until summoned by the keyboard hook;
|
||||
// the accent selector keeps itself hidden (TransparentWindow hides its AppWindow on init).
|
||||
// Quick Accent has no user-visible main window until summoned by the keyboard hook;
|
||||
// the accent selector stays DWM-cloaked while its shown HWND keeps XAML warm.
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
using System;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Windows.Graphics;
|
||||
using CoreSize = PowerAccent.Core.Size;
|
||||
|
||||
@@ -20,9 +21,9 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
// glyph. Its width is measured from the character list (SelectorControl.MeasureContentWidthDip)
|
||||
// plus the space outside the list, NOT derived from the item count: an accent cell is a MINIMUM
|
||||
// of 48 DIP, so a glyph wider than that grows its cell and a count * 48 estimate would size the
|
||||
// window narrower than its own content (issue #49488). The bar is sized twice per summon - once
|
||||
// before Show, then again after the first real layout pass, because only the second measurement
|
||||
// is taken on a templated, non-collapsed subtree.
|
||||
// window narrower than its own content (issue #49488). The measurement is taken after the
|
||||
// surface has been laid out, which the cloaked window (see the constructor) makes possible
|
||||
// before anything is on screen.
|
||||
private const double RowHeightDip = 92; // one row of accent pills (item Height=48 + card border)
|
||||
private const double DescriptionHeightDip = 36; // extra row shown when the Unicode description is on
|
||||
private const double MinItemWidthDip = 48; // one accent cell's minimum (ListViewItem MinWidth=48)
|
||||
@@ -31,30 +32,9 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
// Prevents the fractional pixels that may occur with scaled displays from truncating the character list.
|
||||
private const double LayoutRoundingDip = 1;
|
||||
|
||||
// Upper bound on how long the bar may stay invisible while waiting for its first composed frame
|
||||
// (see PowerAccent_OnChangeDisplay). A CompositionTarget.Rendering handler forces the UI thread
|
||||
// to run every frame, so this timer is NOT the normal path - that is FramesBeforeReveal refresh
|
||||
// intervals. It exists because the tick cadence carries no guarantee (microsoft-ui-xaml#11048)
|
||||
// and because ticking can stop for a locked or fully occluded session. Treat it as a floor, not
|
||||
// a deadline: DispatcherQueueTimer tasks run at a priority lower than idle. On that path the bar
|
||||
// simply appears the way it used to.
|
||||
private const int RevealTimeoutMs = 150;
|
||||
|
||||
// Composed frames to wait before unveiling. The bar is transparent until Reveal(), so neither of
|
||||
// these frames draws it; they buy settling time for the resize below and for the surface's
|
||||
// Collapsed -> Visible flip, so that the frame which first rasterizes the bar (the one after
|
||||
// Opacity = 1) already has the correct client area. Rendering is not tied to any specific
|
||||
// element, so a tick is evidence that a frame elapsed - not that this subtree was composed.
|
||||
private const int FramesBeforeReveal = 2;
|
||||
|
||||
private readonly Core.PowerAccent _powerAccent;
|
||||
private readonly DispatcherQueueTimer _revealTimer;
|
||||
private int _selectedIndex = -1;
|
||||
private int _showGeneration;
|
||||
private int _revealGeneration = -1;
|
||||
private double _measuredContentWidthDip = -1;
|
||||
private int _renderedFrames;
|
||||
private bool _active;
|
||||
|
||||
// The view model lives on the SelectorControl (the x:Bind target); expose it here for the
|
||||
// PowerAccent event handlers that populate the accent list and description.
|
||||
@@ -74,10 +54,13 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
// SubscribeSurfaceTo forwards to the inner surface so it follows this window's Show/Hide.
|
||||
Selector.SubscribeSurfaceTo(this);
|
||||
|
||||
_revealTimer = DispatcherQueue.CreateTimer();
|
||||
_revealTimer.IsRepeating = false;
|
||||
_revealTimer.Interval = TimeSpan.FromMilliseconds(RevealTimeoutMs);
|
||||
_revealTimer.Tick += (_, _) => Reveal();
|
||||
// Cloak the overlay instead of hiding it. A hidden WinUI 3 window renders nothing, so the
|
||||
// bar would become visible while the characters of the new summon are still un-laid-out and
|
||||
// the first frames would show the previous summon's content - issue #49489. Cloaked, the
|
||||
// window stays shown (and therefore laid out and painted) while invisible, so every summon
|
||||
// builds its bar out of sight and Reveal() never has a wrong frame to put on screen. This
|
||||
// also warms the XAML tree up now rather than on the first summon.
|
||||
EnableCloakedHide();
|
||||
|
||||
_powerAccent = new Core.PowerAccent(RunOnUiThread);
|
||||
_powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay;
|
||||
@@ -108,14 +91,11 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
{
|
||||
if (!isActive)
|
||||
{
|
||||
_active = false;
|
||||
// Invalidate any layout callback still queued for the summon being dismissed, so it
|
||||
// cannot reveal a bar that is on its way out. Every dismissal bumps the counter and
|
||||
// every summon captures it, so this is the only liveness check the callback needs.
|
||||
_showGeneration++;
|
||||
|
||||
// Drop any reveal still pending for the summon being dismissed. Same motivation as
|
||||
// releasing always-on-top below: the Rendering handler forces the UI thread to run every
|
||||
// frame, so it must not outlive the visible bar.
|
||||
CancelPendingReveal();
|
||||
|
||||
// Release always-on-top before hiding so the dormant overlay does not keep a discrete
|
||||
// GPU awake on hybrid-graphics laptops (issue #34849 / PR #41044). IsAlwaysOnTop is the
|
||||
// WinUIEx WindowEx property (same as the sibling PowerDisplay).
|
||||
@@ -130,7 +110,6 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
_active = true;
|
||||
int generation = ++_showGeneration;
|
||||
ViewModel.ShowDescription = _powerAccent.ShowUnicodeDescription;
|
||||
|
||||
@@ -145,52 +124,52 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
? _powerAccent.CharacterDescriptions[_selectedIndex]
|
||||
: string.Empty;
|
||||
|
||||
// Show the bar transparent and unveil it once it has actually been drawn. A hidden WinUI 3
|
||||
// window renders nothing, so the HWND would otherwise become visible while the new
|
||||
// characters are still un-laid-out, and the first frames would show the previous bar at its
|
||||
// previous size, clipped by the new (already correct) client area - issue #49489. This is
|
||||
// the WinUI 3 counterpart of the WPF fix in #46593, which rendered the toolbar off screen
|
||||
// and only then moved it into view.
|
||||
Selector.Opacity = 0;
|
||||
|
||||
// Always-on-top only while shown, so the overlay sits above the foreground app (Show uses
|
||||
// SW_SHOWNA and never activates it); released on hide (see above). Then size and show.
|
||||
// SW_SHOWNA and never activates it); released on hide (see above). The window is cloaked at
|
||||
// this point, so Show() flips the surface out of Collapsed and lets the new bar lay out
|
||||
// without anything reaching the screen.
|
||||
IsAlwaysOnTop = true;
|
||||
SizeAndPosition(Selector.MeasureContentWidthDip());
|
||||
Show();
|
||||
|
||||
// Arm the fallback deadline synchronously: the bar is transparent from here on, so the
|
||||
// timeout has to be running even if the callback below never gets to run - otherwise a
|
||||
// dropped callback leaves the bar invisible for the whole summon instead of merely late.
|
||||
ArmRevealTimeout(generation);
|
||||
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
|
||||
{
|
||||
if (!_active || generation != _showGeneration)
|
||||
if (generation != _showGeneration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Runs after TransparentWindow.Show has made the window visible and flipped the surface
|
||||
// out of Collapsed, so this is the first point at which the bar can lay out at all -
|
||||
// and ScrollIntoView needs realized containers to land on the right offset.
|
||||
// Runs after TransparentWindow.Show has flipped the surface out of Collapsed, so this is
|
||||
// the first point at which the bar can lay out at all - a Collapsed subtree is never
|
||||
// measured, which is why the size is taken here and not before Show.
|
||||
Selector.UpdateLayout();
|
||||
SizeAndPosition(Selector.MeasureContentWidthDip());
|
||||
|
||||
// The measurement above ran before the surface left Collapsed and, on the first summon
|
||||
// of the process, before its template had ever been applied, so it can report less than
|
||||
// the items really need - in which case GetToolbarWidth silently falls back to the
|
||||
// item-count estimate this whole change exists to replace. Now that a real layout pass
|
||||
// has run, re-measure and re-size when the two disagree. The bar is still at Opacity 0,
|
||||
// so the correction is never seen as a resize.
|
||||
double laidOutContentWidthDip = Selector.MeasureContentWidthDip();
|
||||
if (Math.Abs(laidOutContentWidthDip - _measuredContentWidthDip) > LayoutRoundingDip)
|
||||
// Lay out again at the new window size: ScrollIntoView needs realized containers and the
|
||||
// final viewport to land on the right offset.
|
||||
Selector.UpdateLayout();
|
||||
Selector.ScrollSelectedIntoView(_selectedIndex);
|
||||
|
||||
// UpdateLayout only completes XAML measure/arrange. Wait for the composition commit so
|
||||
// uncloaking cannot expose the previous summon's redirection surface at the new bounds.
|
||||
try
|
||||
{
|
||||
SizeAndPosition(laidOutContentWidthDip);
|
||||
Selector.UpdateLayout();
|
||||
await ElementCompositionPreview.GetElementVisual(Selector).Compositor.RequestCommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to commit the Quick Accent layout before reveal", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
Selector.ScrollSelectedIntoView(_selectedIndex);
|
||||
WaitForFirstFrameThenReveal();
|
||||
if (generation != _showGeneration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything above happened on a window that was shown but cloaked. The commit ensures
|
||||
// DWM's redirection surface already contains this summon's final layout before it is
|
||||
// exposed, so Reveal cannot flash the previous bar at the new bounds (issue #49489).
|
||||
Reveal();
|
||||
});
|
||||
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Core.Telemetry.PowerAccentShowAccentMenuEvent());
|
||||
@@ -216,8 +195,6 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
// max usable width so long lists scroll. The Unicode description row needs room for a
|
||||
// readable line, so it widens a short bar to the WPF original's minimum (the accent bar
|
||||
// itself stays centered within the wider window).
|
||||
_measuredContentWidthDip = measuredContentWidthDip;
|
||||
|
||||
double widthDip = _powerAccent.GetDisplayWidth(
|
||||
measuredContentWidthDip,
|
||||
ViewModel.Characters.Count,
|
||||
@@ -246,72 +223,8 @@ public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
FlyoutWindowHelper.MoveAndResizeOnDisplay(this, display, rect);
|
||||
}
|
||||
|
||||
// Starts the fallback deadline for the reveal, tagged with the summon that armed it.
|
||||
private void ArmRevealTimeout(int generation)
|
||||
{
|
||||
// Cancel before re-tagging, not after. The Stop() inside CancelPendingReveal is what kills
|
||||
// the previous summon's watchdog, and that watchdog was the only remaining path that would
|
||||
// have driven its still-attached Rendering handler through Reveal(). Leaving the handler on
|
||||
// would let it unveil THIS summon after FramesBeforeReveal ticks - before the layout
|
||||
// callback below has run - and the generation guard in Reveal() cannot reject it, because
|
||||
// the two assignments underneath put _revealGeneration and _showGeneration back in sync.
|
||||
CancelPendingReveal();
|
||||
|
||||
_revealGeneration = generation;
|
||||
_renderedFrames = 0;
|
||||
|
||||
// Restart rather than extend: DispatcherQueueTimer does not document what Start() does to a
|
||||
// timer that is already running, so the deadline is reset explicitly.
|
||||
_revealTimer.Start();
|
||||
}
|
||||
|
||||
// Drops a pending reveal. The watchdog and the per-frame handler always come off together: the
|
||||
// handler forces the UI thread to run every frame, and the watchdog is what guarantees it is
|
||||
// detached on a session where no frames arrive at all.
|
||||
private void CancelPendingReveal()
|
||||
{
|
||||
_revealTimer.Stop();
|
||||
CompositionTarget.Rendering -= OnRenderingBeforeReveal;
|
||||
}
|
||||
|
||||
// Unveils the bar once the compositor has drawn it, or after RevealTimeoutMs if it never does.
|
||||
private void WaitForFirstFrameThenReveal()
|
||||
{
|
||||
_renderedFrames = 0;
|
||||
|
||||
// Re-arms rather than stacks: a summon that lands while an earlier one is still waiting
|
||||
// reuses the same handler, so there is only ever one pending reveal.
|
||||
CompositionTarget.Rendering -= OnRenderingBeforeReveal;
|
||||
CompositionTarget.Rendering += OnRenderingBeforeReveal;
|
||||
}
|
||||
|
||||
private void OnRenderingBeforeReveal(object sender, object e)
|
||||
{
|
||||
if (++_renderedFrames < FramesBeforeReveal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Reveal();
|
||||
}
|
||||
|
||||
private void Reveal()
|
||||
{
|
||||
// Unconditional: the handler forces the UI thread to run every frame, so it has to come off
|
||||
// even when the reveal itself is dropped as stale just below.
|
||||
CancelPendingReveal();
|
||||
|
||||
// Same guard as the layout callback. A reveal armed by an earlier summon must not unveil a
|
||||
// newer one before its own layout pass has run - that is exactly the stale frame of #49489.
|
||||
if (_active && _revealGeneration == _showGeneration)
|
||||
{
|
||||
Selector.Opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CancelPendingReveal();
|
||||
_powerAccent.SaveUsageInfo();
|
||||
_powerAccent.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -41,11 +41,10 @@ public sealed partial class SelectorControl : UserControl
|
||||
/// The cell is a <c>MinWidth</c> of 48, not a fixed 48: a glyph wider than that (₹, ‰, ﷼, a CJK
|
||||
/// fallback) grows its cell, so the bar has to be measured rather than derived from the item
|
||||
/// count. The measurement is taken explicitly instead of read from the last layout pass because
|
||||
/// the bar is rebuilt on every summon while the window is still hidden, so no pass has run for
|
||||
/// the new items; measuring against an infinite width also yields the true content width rather
|
||||
/// than whatever the ScrollViewer inside the ListView's own template would have clipped it to.
|
||||
/// A caller that measures before the surface has been templated and laid out may get less than
|
||||
/// the items need, so <c>MainWindow</c> measures again after its first layout pass.
|
||||
/// the bar is rebuilt on every summon while its shown window is still cloaked, so the caller
|
||||
/// first lays out the newly visible surface off screen. Measuring against an infinite width also
|
||||
/// yields the true content width rather than whatever the ScrollViewer inside the ListView's own
|
||||
/// template would have clipped it to.
|
||||
/// </remarks>
|
||||
internal double MeasureContentWidthDip()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user