[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:
moooyo
2026-08-11 12:06:19 +08:00
committed by GitHub
parent e48152c52d
commit fe4a86a54d
4 changed files with 224 additions and 146 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);

View File

@@ -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()
{