diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CmdPalMainControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CmdPalMainControl.xaml.cs index 465cf0e58e..6b75d1243a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CmdPalMainControl.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CmdPalMainControl.xaml.cs @@ -5,11 +5,11 @@ using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Services; -using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using Windows.UI; -using WinUIEx; namespace Microsoft.CmdPal.UI.Controls; @@ -18,8 +18,11 @@ namespace Microsoft.CmdPal.UI.Controls; /// corners, border, shadow and system backdrop. The HWND that hosts it is borderless /// and transparent, so all the chrome lives here instead of in window non-client area. /// -public sealed partial class CmdPalMainControl : UserControl +public sealed partial class CmdPalMainControl : UserControl, IDisposable { + private readonly TintedControllerBackdrop _backdrop = new(); + private Color _cardFallbackBackground; + public static readonly DependencyProperty MainContentProperty = DependencyProperty.Register( nameof(MainContent), @@ -103,6 +106,8 @@ public sealed partial class CmdPalMainControl : UserControl public CmdPalMainControl() { this.InitializeComponent(); + _backdrop.BackdropAttachmentChanged += OnBackdropAttachmentChanged; + BackdropElement.SystemBackdrop = _backdrop; } /// @@ -142,19 +147,21 @@ public sealed partial class CmdPalMainControl : UserControl /// public void SetIsInputActive(bool isActive) { - if (BackdropElement.SystemBackdrop is TintedControllerBackdrop tinted) - { - tinted.IsInputActive = isActive; - } + _backdrop.IsInputActive = isActive; } /// - /// Detaches any backdrop from the embedded element. Used during shutdown to release the - /// underlying controller eagerly. + /// Releases the active controller on the XAML thread while keeping the projected + /// backdrop target rooted until WinUI disconnects it during shutdown. /// public void ClearBackdrop() { - BackdropElement.SystemBackdrop = null; + Dispose(); + } + + public void Dispose() + { + _backdrop.Dispose(); } /// @@ -168,64 +175,57 @@ public sealed partial class CmdPalMainControl : UserControl { try { - BackdropElement.SystemBackdrop = CreateBackdrop(backdrop, kind, isImageMode, hasColorization); + // The border fill sits underneath SystemBackdropElement and remains a ready + // fallback if a controller or composition brush cannot attach. + _cardFallbackBackground = CreateCardBackground(backdrop, kind); + SetCardBackground(_cardFallbackBackground); + + // Update the controller behind the one long-lived SystemBackdrop. Replacing the + // SystemBackdrop property would create short-lived, thread-affine target + // projections that C#/WinRT can otherwise release from its finalizer thread. + _backdrop.Update(backdrop, kind, isImageMode, hasColorization); + UpdateCardBackground(_backdrop.IsBackdropAttached); } catch (Exception ex) { + SetCardBackground(backdrop.FallbackColor); Logger.LogError("Failed to apply backdrop to CmdPalMainControl", ex); } } - private static Microsoft.UI.Xaml.Media.SystemBackdrop? CreateBackdrop(BackdropParameters backdrop, BackdropControllerKind kind, bool isImageMode, bool hasColorization) + private void SetCardBackground(Color color) { - // Image mode: don't tint here, BlurImageControl handles it (avoids double-tinting). - var effectiveTintOpacity = isImageMode ? 0.0f : backdrop.EffectiveOpacity; - - switch (kind) + if (CardBorder.Background is SolidColorBrush background) { - case BackdropControllerKind.Solid: - var solidTint = Color.FromArgb( - (byte)(backdrop.EffectiveOpacity * 255), - backdrop.TintColor.R, - backdrop.TintColor.G, - backdrop.TintColor.B); - return new TransparentTintBackdrop { TintColor = solidTint }; - - case BackdropControllerKind.Mica: - case BackdropControllerKind.MicaAlt: - if (!MicaController.IsSupported()) - { - return new TransparentTintBackdrop { TintColor = backdrop.FallbackColor }; - } - - return new TintedMicaBackdrop - { - Kind = kind == BackdropControllerKind.MicaAlt ? MicaKind.BaseAlt : MicaKind.Base, - ApplyTint = hasColorization || isImageMode, - TintColor = backdrop.TintColor, - TintOpacity = effectiveTintOpacity, - FallbackColor = backdrop.FallbackColor, - LuminosityOpacity = backdrop.EffectiveLuminosityOpacity, - }; - - case BackdropControllerKind.Acrylic: - case BackdropControllerKind.AcrylicThin: - default: - if (!DesktopAcrylicController.IsSupported()) - { - return new TransparentTintBackdrop { TintColor = backdrop.FallbackColor }; - } - - return new TintedDesktopAcrylicBackdrop - { - Kind = kind == BackdropControllerKind.AcrylicThin - ? DesktopAcrylicKind.Thin - : DesktopAcrylicKind.Default, - TintColor = backdrop.TintColor, - TintOpacity = effectiveTintOpacity, - FallbackColor = backdrop.FallbackColor, - LuminosityOpacity = backdrop.EffectiveLuminosityOpacity, - }; + background.Color = color; + } + else + { + CardBorder.Background = new SolidColorBrush(color); } } + + private void OnBackdropAttachmentChanged(bool isBackdropAttached) + { + UpdateCardBackground(isBackdropAttached); + } + + private void UpdateCardBackground(bool isBackdropAttached) + { + SetCardBackground(isBackdropAttached ? Colors.Transparent : _cardFallbackBackground); + } + + private static Color CreateCardBackground(BackdropParameters backdrop, BackdropControllerKind kind) + { + if (kind == BackdropControllerKind.Solid) + { + return Color.FromArgb( + (byte)(backdrop.EffectiveOpacity * 255), + backdrop.TintColor.R, + backdrop.TintColor.G, + backdrop.TintColor.B); + } + + return backdrop.FallbackColor; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedBackdrops.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedBackdrops.cs index cc45c36db0..f8825a574a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedBackdrops.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedBackdrops.cs @@ -2,89 +2,619 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; +using ManagedCommon; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.UI.Composition; using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; using Windows.UI; +using WindowsCompositionColorBrush = Windows.UI.Composition.CompositionColorBrush; +using WindowsCompositionCompositor = Windows.UI.Composition.Compositor; namespace Microsoft.CmdPal.UI.Controls; /// -/// Base class for tinted backdrops that wrap a controller from -/// so they can be applied -/// to a single control via . +/// A composition-backed whose material and tint can be +/// updated without replacing the backdrop attached to a SystemBackdropElement. /// /// -/// The stock / classes -/// don't expose tint color / opacity / luminosity customization. This base type plugs -/// the lower-level controllers into the new -/// extensibility surface so we can keep all of CmdPal's theme-driven tinting. +/// The projected target owns a +/// thread-affine native ContentExternalBackdropLink. Keeping the target in +/// for its entire connected lifetime prevents C#/WinRT from +/// releasing that link on the finalizer thread while CmdPal switches materials. /// -internal abstract partial class TintedControllerBackdrop : SystemBackdrop +internal sealed partial class TintedControllerBackdrop : SystemBackdrop, IDisposable { - private SystemBackdropConfiguration? _config; + private readonly Dictionary _targets = []; - public Color TintColor { get; init; } + private BackdropSettings? _settings; + private bool _isBackdropAttached; + private bool _isInputActive = true; + private bool _isDisposed; - public float TintOpacity { get; init; } + public event Action? BackdropAttachmentChanged; - public Color FallbackColor { get; init; } - - public float LuminosityOpacity { get; init; } + public bool IsBackdropAttached => _isBackdropAttached; /// - /// Gets a value indicating whether tint properties should be applied. Mica without - /// colorization wants the system defaults; in that case set this to false. - /// - public bool ApplyTint { get; init; } = true; - - /// - /// Gets or sets a value indicating whether the host window is currently activated. The - /// system uses this to decide between the active and inactive backdrop appearance. + /// Gets or sets a value indicating whether the host window is currently activated. /// public bool IsInputActive { - get => _config?.IsInputActive ?? true; + get => _isInputActive; set { - if (_config is not null) + _isInputActive = value; + + foreach (var target in _targets.Values) { - _config.IsInputActive = value; + target?.SetIsInputActive(value); } } } - protected SystemBackdropConfiguration? Configuration => _config; + /// + /// Updates the material rendered by every connected target. Controller cleanup happens + /// synchronously on the XAML thread; direct brush attachment is deferred through that + /// thread's dispatcher when the native backdrop link is being handed off. + /// + public void Update(BackdropParameters backdrop, BackdropControllerKind kind, bool isImageMode, bool hasColorization) + { + if (_isDisposed) + { + return; + } + + var settings = new BackdropSettings( + kind, + Color.FromArgb( + (byte)(backdrop.EffectiveOpacity * 255), + backdrop.TintColor.R, + backdrop.TintColor.G, + backdrop.TintColor.B), + backdrop.TintColor, + isImageMode ? 0.0f : backdrop.EffectiveOpacity, + backdrop.FallbackColor, + backdrop.EffectiveLuminosityOpacity, + hasColorization || isImageMode); + _settings = settings; + + foreach (var (target, state) in _targets) + { + state?.Apply(target, settings); + } + + UpdateBackdropAttachmentState(); + } protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot) { base.OnTargetConnected(connectedTarget, xamlRoot); - _config = new SystemBackdropConfiguration + + // Root the projected target before creating any other WinRT objects. The target + // projection must not be finalized while its native backdrop link is connected. + _targets[connectedTarget] = null; + + try { - IsInputActive = true, - Theme = xamlRoot.Content is FrameworkElement fe - ? ToBackdropTheme(fe.ActualTheme) - : SystemBackdropTheme.Default, - }; - AttachController(connectedTarget, xamlRoot); + var target = new BackdropTarget(xamlRoot, _isInputActive, UpdateBackdropAttachmentState); + _targets[connectedTarget] = target; + + if (!_isDisposed && _settings is { } settings) + { + target.Apply(connectedTarget, settings); + } + } + catch (Exception ex) + { + // Do not let an attach failure escape after the base class has registered the + // target. XAML can still disconnect it later without corrupting base state. + Logger.LogError("Failed to connect controller-backed system backdrop", ex); + } + + UpdateBackdropAttachmentState(); } protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget) { - DetachController(disconnectedTarget); - _config = null; - base.OnTargetDisconnected(disconnectedTarget); + try + { + base.OnTargetDisconnected(disconnectedTarget); + } + finally + { + if (_targets.Remove(disconnectedTarget, out var target)) + { + target?.Close(disconnectedTarget); + } + + UpdateBackdropAttachmentState(); + } } - protected abstract void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot); - - protected abstract void DetachController(ICompositionSupportsSystemBackdrop target); - - private static SystemBackdropTheme ToBackdropTheme(ElementTheme theme) => theme switch + public void Dispose() { - ElementTheme.Dark => SystemBackdropTheme.Dark, - ElementTheme.Light => SystemBackdropTheme.Light, - _ => SystemBackdropTheme.Default, - }; + if (_isDisposed) + { + return; + } + + _isDisposed = true; + _settings = null; + + // Keep the connected targets rooted until XAML disconnects them. Only the + // controller or color brush is closed here, synchronously on the owning UI thread. + foreach (var (target, state) in _targets) + { + state?.Close(target); + } + + UpdateBackdropAttachmentState(); + } + + private void UpdateBackdropAttachmentState() + { + var isBackdropAttached = false; + + foreach (var target in _targets.Values) + { + if (target?.IsBackdropAttached == true) + { + isBackdropAttached = true; + break; + } + } + + if (_isBackdropAttached == isBackdropAttached) + { + return; + } + + _isBackdropAttached = isBackdropAttached; + + try + { + BackdropAttachmentChanged?.Invoke(isBackdropAttached); + } + catch (Exception ex) + { + Logger.LogError("Failed to update system backdrop fallback", ex); + } + } + + private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) => + xamlRoot.Content is FrameworkElement rootElement + ? rootElement.ActualTheme switch + { + ElementTheme.Dark => SystemBackdropTheme.Dark, + ElementTheme.Light => SystemBackdropTheme.Light, + _ => SystemBackdropTheme.Default, + } + : SystemBackdropTheme.Default; + + private readonly record struct BackdropSettings( + BackdropControllerKind Kind, + Color SolidColor, + Color TintColor, + float TintOpacity, + Color FallbackColor, + float LuminosityOpacity, + bool ApplyTint); + + private sealed class BackdropTarget + { + private const int SolidAttachRetryCount = 2; + + private readonly XamlRoot _xamlRoot; + private readonly SystemBackdropConfiguration _configuration; + private readonly Action _backdropAttachmentChanged; + + private BackdropSettings? _appliedSettings; + private BackdropSettings? _queuedSettings; + private WindowsCompositionCompositor? _solidColorCompositor; + private WindowsCompositionColorBrush? _solidColorBrush; + private MicaController? _micaController; + private DesktopAcrylicController? _acrylicController; + private int _queuedSolidAttachRetries; + private bool _backdropHasTarget; + private bool _isApplyQueued; + private bool _isClosed; + + public bool IsBackdropAttached => _backdropHasTarget; + + public BackdropTarget(XamlRoot xamlRoot, bool isInputActive, Action backdropAttachmentChanged) + { + _xamlRoot = xamlRoot; + _backdropAttachmentChanged = backdropAttachmentChanged; + _configuration = new SystemBackdropConfiguration + { + IsInputActive = isInputActive, + Theme = ResolveTheme(xamlRoot), + }; + } + + public void SetIsInputActive(bool isInputActive) + { + if (!_isClosed) + { + _configuration.IsInputActive = isInputActive; + } + } + + public void Apply(ICompositionSupportsSystemBackdrop target, BackdropSettings settings) + { + if (_isClosed) + { + return; + } + + if (_isApplyQueued) + { + // Keep only the newest theme choice while a previous controller-to-brush + // handoff is waiting for the current dispatcher callback to unwind. + _queuedSettings = settings; + _queuedSolidAttachRetries = settings.Kind == BackdropControllerKind.Solid + ? SolidAttachRetryCount + : 0; + return; + } + + Apply( + target, + settings, + deferSolidAttach: true, + solidAttachRetriesRemaining: SolidAttachRetryCount); + } + + public void Close(ICompositionSupportsSystemBackdrop target) + { + if (_isClosed) + { + return; + } + + _isClosed = true; + _queuedSettings = null; + DetachBackdrop(target); + } + + private void Apply( + ICompositionSupportsSystemBackdrop target, + BackdropSettings settings, + bool deferSolidAttach, + int solidAttachRetriesRemaining) + { + _configuration.Theme = ResolveTheme(_xamlRoot); + + if (_appliedSettings == settings) + { + return; + } + + if (settings.Kind == BackdropControllerKind.Solid && _solidColorBrush is not null && _backdropHasTarget) + { + try + { + _solidColorBrush.Color = settings.SolidColor; + _appliedSettings = settings; + return; + } + catch (Exception ex) + { + Logger.LogError("Failed to update solid system backdrop tint", ex); + } + } + + DetachBackdrop(target); + + // SystemBackdropElement can temporarily reject a direct composition brush while + // the controller removed above is still unwinding its native backdrop link. + // Let that handoff finish before assigning the color brush. + if (deferSolidAttach && + settings.Kind == BackdropControllerKind.Solid && + QueueApply(target, settings, SolidAttachRetryCount)) + { + return; + } + + try + { + switch (settings.Kind) + { + case BackdropControllerKind.Solid: + AttachSolidColorBrush(target, settings); + break; + + case BackdropControllerKind.Mica: + case BackdropControllerKind.MicaAlt: + AttachMicaController(target, settings); + break; + + case BackdropControllerKind.Acrylic: + case BackdropControllerKind.AcrylicThin: + default: + AttachAcrylicController(target, settings); + break; + } + + _appliedSettings = settings; + } + catch (UnauthorizedAccessException ex) when (settings.Kind == BackdropControllerKind.Solid) + { + DetachBackdrop(target); + + if (solidAttachRetriesRemaining > 0 && + QueueApply(target, settings, solidAttachRetriesRemaining - 1)) + { + return; + } + + Logger.LogWarning( + $"Solid backdrop target remained unavailable after the native handoff; using the fallback background. HRESULT: 0x{ex.HResult:X8}."); + } + catch (Exception ex) + { + // A failed controller or brush remains owned by this target state and is closed + // immediately on the XAML thread. The SystemBackdrop target stays rooted. + DetachBackdrop(target); + Logger.LogError("Failed to apply composition-backed system backdrop", ex); + } + } + + private bool QueueApply( + ICompositionSupportsSystemBackdrop target, + BackdropSettings settings, + int solidAttachRetriesRemaining) + { + if (_isClosed) + { + return false; + } + + _queuedSettings = settings; + _queuedSolidAttachRetries = solidAttachRetriesRemaining; + + if (_isApplyQueued) + { + return true; + } + + _isApplyQueued = true; + if (_xamlRoot.Content.DispatcherQueue.TryEnqueue(() => ApplyQueued(target))) + { + return true; + } + + _isApplyQueued = false; + _queuedSettings = null; + _queuedSolidAttachRetries = 0; + return false; + } + + private void ApplyQueued(ICompositionSupportsSystemBackdrop target) + { + _isApplyQueued = false; + + var settings = _queuedSettings; + var solidAttachRetriesRemaining = _queuedSolidAttachRetries; + _queuedSettings = null; + _queuedSolidAttachRetries = 0; + + if (_isClosed || settings is null) + { + return; + } + + try + { + Apply( + target, + settings.Value, + deferSolidAttach: false, + solidAttachRetriesRemaining: solidAttachRetriesRemaining); + } + catch (Exception ex) + { + DetachBackdrop(target); + Logger.LogError("Failed to apply queued system backdrop", ex); + } + finally + { + _backdropAttachmentChanged(); + } + } + + private void DetachBackdrop(ICompositionSupportsSystemBackdrop target) + { + _appliedSettings = null; + + var solidColorCompositor = _solidColorCompositor; + var solidColorBrush = _solidColorBrush; + var micaController = _micaController; + var acrylicController = _acrylicController; + var backdropHasTarget = _backdropHasTarget; + + _solidColorCompositor = null; + _solidColorBrush = null; + _micaController = null; + _acrylicController = null; + _backdropHasTarget = false; + + if (solidColorBrush is not null) + { + RemoveTargetAndDispose(solidColorBrush, target, backdropHasTarget); + } + + if (solidColorCompositor is not null) + { + Dispose(solidColorCompositor); + } + + if (micaController is not null) + { + RemoveTargetAndDispose(micaController, target, backdropHasTarget); + } + + if (acrylicController is not null) + { + RemoveTargetAndDispose(acrylicController, target, backdropHasTarget); + } + } + + private void AttachSolidColorBrush(ICompositionSupportsSystemBackdrop target, BackdropSettings settings) + { + // SystemBackdrop uses Windows.UI.Composition brushes even though its target is + // projected through Microsoft.UI.Composition. Create and retain the matching + // compositor on the owning XAML thread so neither projection reaches finalization. + var compositor = new WindowsCompositionCompositor(); + _solidColorCompositor = compositor; + var brush = compositor.CreateColorBrush(settings.SolidColor); + _solidColorBrush = brush; + _backdropHasTarget = true; + target.SystemBackdrop = brush; + } + + private void AttachMicaController(ICompositionSupportsSystemBackdrop target, BackdropSettings settings) + { + if (!MicaController.IsSupported()) + { + return; + } + + var controller = new MicaController + { + Kind = settings.Kind == BackdropControllerKind.MicaAlt ? MicaKind.BaseAlt : MicaKind.Base, + }; + _micaController = controller; + + if (settings.ApplyTint) + { + controller.TintColor = settings.TintColor; + controller.TintOpacity = settings.TintOpacity; + controller.FallbackColor = settings.FallbackColor; + controller.LuminosityOpacity = settings.LuminosityOpacity; + } + + controller.SetSystemBackdropConfiguration(_configuration); + _backdropHasTarget = true; + controller.AddSystemBackdropTarget(target); + } + + private void AttachAcrylicController(ICompositionSupportsSystemBackdrop target, BackdropSettings settings) + { + if (!DesktopAcrylicController.IsSupported()) + { + return; + } + + var controller = new DesktopAcrylicController + { + Kind = settings.Kind == BackdropControllerKind.AcrylicThin + ? DesktopAcrylicKind.Thin + : DesktopAcrylicKind.Default, + TintColor = settings.TintColor, + TintOpacity = settings.TintOpacity, + FallbackColor = settings.FallbackColor, + LuminosityOpacity = settings.LuminosityOpacity, + }; + _acrylicController = controller; + + controller.SetSystemBackdropConfiguration(_configuration); + _backdropHasTarget = true; + controller.AddSystemBackdropTarget(target); + } + + private static void RemoveTargetAndDispose(WindowsCompositionColorBrush brush, ICompositionSupportsSystemBackdrop target, bool backdropHasTarget) + { + try + { + if (backdropHasTarget) + { + target.SystemBackdrop = null; + } + } + catch (Exception ex) + { + Logger.LogError("Failed to remove solid system backdrop target", ex); + } + finally + { + try + { + brush.Dispose(); + } + catch (Exception ex) + { + Logger.LogError("Failed to dispose solid system backdrop brush", ex); + } + } + } + + private static void Dispose(WindowsCompositionCompositor compositor) + { + try + { + compositor.Dispose(); + } + catch (Exception ex) + { + Logger.LogError("Failed to dispose solid system backdrop compositor", ex); + } + } + + private static void RemoveTargetAndDispose(MicaController controller, ICompositionSupportsSystemBackdrop target, bool backdropHasTarget) + { + try + { + if (backdropHasTarget) + { + controller.RemoveSystemBackdropTarget(target); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to remove Mica system backdrop target", ex); + } + finally + { + try + { + controller.Dispose(); + } + catch (Exception ex) + { + Logger.LogError("Failed to dispose Mica system backdrop controller", ex); + } + } + } + + private static void RemoveTargetAndDispose(DesktopAcrylicController controller, ICompositionSupportsSystemBackdrop target, bool backdropHasTarget) + { + try + { + if (backdropHasTarget) + { + controller.RemoveSystemBackdropTarget(target); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to remove acrylic system backdrop target", ex); + } + finally + { + try + { + controller.Dispose(); + } + catch (Exception ex) + { + Logger.LogError("Failed to dispose acrylic system backdrop controller", ex); + } + } + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedDesktopAcrylicBackdrop.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedDesktopAcrylicBackdrop.cs deleted file mode 100644 index 781c946bee..0000000000 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedDesktopAcrylicBackdrop.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.UI.Composition; -using Microsoft.UI.Composition.SystemBackdrops; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Media; - -namespace Microsoft.CmdPal.UI.Controls; - -/// -/// A tinted exposed as a -/// so it can be hosted by . -/// -internal sealed partial class TintedDesktopAcrylicBackdrop : TintedControllerBackdrop, IDisposable -{ - private DesktopAcrylicController? _controller; - - public DesktopAcrylicKind Kind { get; init; } = DesktopAcrylicKind.Default; - - protected override void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot) - { - if (!DesktopAcrylicController.IsSupported()) - { - return; - } - - _controller = new DesktopAcrylicController - { - Kind = Kind, - TintColor = TintColor, - TintOpacity = TintOpacity, - FallbackColor = FallbackColor, - LuminosityOpacity = LuminosityOpacity, - }; - - _controller.AddSystemBackdropTarget(target); - _controller.SetSystemBackdropConfiguration(Configuration); - } - - protected override void DetachController(ICompositionSupportsSystemBackdrop target) - { - if (_controller is not null) - { - _controller.RemoveSystemBackdropTarget(target); - _controller.Dispose(); - _controller = null; - } - } - - public void Dispose() - { - _controller?.Dispose(); - _controller = null; - } -} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedMicaBackdrop.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedMicaBackdrop.cs deleted file mode 100644 index c29a45522f..0000000000 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/TintedMicaBackdrop.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.UI.Composition; -using Microsoft.UI.Composition.SystemBackdrops; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Media; -using Windows.UI; - -namespace Microsoft.CmdPal.UI.Controls; - -/// -/// A tinted exposed as a -/// so it can be hosted by . -/// -internal sealed partial class TintedMicaBackdrop : TintedControllerBackdrop, IDisposable -{ - private MicaController? _controller; - - public MicaKind Kind { get; init; } = MicaKind.Base; - - protected override void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot) - { - if (!MicaController.IsSupported()) - { - return; - } - - _controller = new MicaController { Kind = Kind }; - - // Only set tint properties when colorization is active. - // Otherwise let the system handle light/dark theme defaults automatically. - if (ApplyTint) - { - _controller.TintColor = TintColor; - _controller.TintOpacity = TintOpacity; - _controller.FallbackColor = FallbackColor; - _controller.LuminosityOpacity = LuminosityOpacity; - } - - _controller.AddSystemBackdropTarget(target); - _controller.SetSystemBackdropConfiguration(Configuration); - } - - protected override void DetachController(ICompositionSupportsSystemBackdrop target) - { - if (_controller is not null) - { - _controller.RemoveSystemBackdropTarget(target); - _controller.Dispose(); - _controller = null; - } - } - - public void Dispose() - { - _controller?.Dispose(); - _controller = null; - } -} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 1931a7f2b2..6575a2c9a4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -96,6 +96,7 @@ public sealed partial class MainWindow : WindowEx, private int _sessionErrorCount; private bool _isUpdatingBackdrop; + private bool _isBackdropUpdatePending; private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan; // Tracks the chrome mode currently applied to the HWND. Nullable so the first @@ -237,12 +238,39 @@ public sealed partial class MainWindow : WindowEx, private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e) { - UpdateBackdrop(); + ScheduleBackdropUpdate(); } private void RootElement_ActualThemeChanged(FrameworkElement sender, object args) { - DispatcherQueue.TryEnqueue(UpdateBackdrop); + ScheduleBackdropUpdate(); + } + + private void ScheduleBackdropUpdate() + { + // A theme reload changes RequestedTheme several times to force WinUI to refresh + // its resources. Coalesce the resulting ThemeChanged / ActualThemeChanged events + // so the SystemBackdropElement is only updated once with the final theme. + if (_isBackdropUpdatePending) + { + return; + } + + _isBackdropUpdatePending = true; + if (!DispatcherQueue.TryEnqueue(() => + { + try + { + UpdateBackdrop(); + } + finally + { + _isBackdropUpdatePending = false; + } + })) + { + _isBackdropUpdatePending = false; + } } private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) @@ -1153,9 +1181,10 @@ public sealed partial class MainWindow : WindowEx, private void DisposeAcrylic() { - // The backdrop controllers now live on the SystemBackdropElement inside - // CmdPalMainControl. Clearing its SystemBackdrop fires OnTargetDisconnected on the - // current backdrop, which removes targets and disposes the underlying controller. + // Backdrop resources are thread-affine. ClearBackdrop closes the active controller or + // brush on the XAML thread, but leaves SystemBackdrop assigned so its target stays rooted. + // Clearing it can let C#/WinRT finalize ContentExternalBackdropLink off-thread, which + // fail-fasts with RPC_E_WRONG_THREAD (0x8001010E). try { RootElement?.ClearBackdrop();