mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
## Summary This PR adds: - Backdrop material customization - Alongside acrylic, the following options are now available: - Transparent background - Mica background - Background material opacity - Lets you control how transparent the background is ## Pictures? Pictures! <img width="1491" height="928" alt="image" src="https://github.com/user-attachments/assets/ff4e9e06-fcf1-4f05-bc0a-fb70dc4f39be" /> https://github.com/user-attachments/assets/84e83279-afab-481e-b904-f054318c5d2f <img width="977" height="628" alt="image" src="https://github.com/user-attachments/assets/241a228d-af3f-448a-94a6-0a282218bd8c" /> ## PR Checklist - [x] Closes: #44197 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
449 lines
15 KiB
C#
449 lines
15 KiB
C#
// 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 System.Numerics;
|
|
using ManagedCommon;
|
|
using Microsoft.Graphics.Canvas.Effects;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Composition;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Hosting;
|
|
using Microsoft.UI.Xaml.Media;
|
|
using Windows.UI;
|
|
|
|
namespace Microsoft.CmdPal.UI.Controls;
|
|
|
|
internal sealed partial class BlurImageControl : Control
|
|
{
|
|
private const string ImageSourceParameterName = "ImageSource";
|
|
|
|
private const string BrightnessEffectName = "Brightness";
|
|
private const string BrightnessOverlayEffectName = "BrightnessOverlay";
|
|
private const string BlurEffectName = "Blur";
|
|
private const string TintBlendEffectName = "TintBlend";
|
|
private const string TintEffectName = "Tint";
|
|
|
|
#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
|
|
private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
|
|
private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
|
|
private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
|
|
private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
|
|
private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
|
|
#pragma warning restore CA1507
|
|
|
|
private static readonly string[] AnimatableProperties = [
|
|
BrightnessSource1AmountEffectProperty,
|
|
BrightnessSource2AmountEffectProperty,
|
|
BrightnessOverlayColorEffectProperty,
|
|
BlurBlurAmountEffectProperty,
|
|
TintColorEffectProperty
|
|
];
|
|
|
|
public static readonly DependencyProperty ImageSourceProperty =
|
|
DependencyProperty.Register(
|
|
nameof(ImageSource),
|
|
typeof(ImageSource),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(null, OnImageChanged));
|
|
|
|
public static readonly DependencyProperty ImageStretchProperty =
|
|
DependencyProperty.Register(
|
|
nameof(ImageStretch),
|
|
typeof(Stretch),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
|
|
|
|
public static readonly DependencyProperty ImageOpacityProperty =
|
|
DependencyProperty.Register(
|
|
nameof(ImageOpacity),
|
|
typeof(double),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(1.0, OnOpacityChanged));
|
|
|
|
public static readonly DependencyProperty ImageBrightnessProperty =
|
|
DependencyProperty.Register(
|
|
nameof(ImageBrightness),
|
|
typeof(double),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(1.0, OnBrightnessChanged));
|
|
|
|
public static readonly DependencyProperty BlurAmountProperty =
|
|
DependencyProperty.Register(
|
|
nameof(BlurAmount),
|
|
typeof(double),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(0.0, OnBlurAmountChanged));
|
|
|
|
public static readonly DependencyProperty TintColorProperty =
|
|
DependencyProperty.Register(
|
|
nameof(TintColor),
|
|
typeof(Color),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
|
|
|
|
public static readonly DependencyProperty TintIntensityProperty =
|
|
DependencyProperty.Register(
|
|
nameof(TintIntensity),
|
|
typeof(double),
|
|
typeof(BlurImageControl),
|
|
new PropertyMetadata(0.0, OnVisualPropertyChanged));
|
|
|
|
private Compositor? _compositor;
|
|
private SpriteVisual? _effectVisual;
|
|
private CompositionEffectBrush? _effectBrush;
|
|
private CompositionSurfaceBrush? _imageBrush;
|
|
|
|
public BlurImageControl()
|
|
{
|
|
this.DefaultStyleKey = typeof(BlurImageControl);
|
|
this.Loaded += OnLoaded;
|
|
this.SizeChanged += OnSizeChanged;
|
|
}
|
|
|
|
public ImageSource ImageSource
|
|
{
|
|
get => (ImageSource)GetValue(ImageSourceProperty);
|
|
set => SetValue(ImageSourceProperty, value);
|
|
}
|
|
|
|
public Stretch ImageStretch
|
|
{
|
|
get => (Stretch)GetValue(ImageStretchProperty);
|
|
set => SetValue(ImageStretchProperty, value);
|
|
}
|
|
|
|
public double ImageOpacity
|
|
{
|
|
get => (double)GetValue(ImageOpacityProperty);
|
|
set => SetValue(ImageOpacityProperty, value);
|
|
}
|
|
|
|
public double ImageBrightness
|
|
{
|
|
get => (double)GetValue(ImageBrightnessProperty);
|
|
set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
|
|
}
|
|
|
|
public double BlurAmount
|
|
{
|
|
get => (double)GetValue(BlurAmountProperty);
|
|
set => SetValue(BlurAmountProperty, value);
|
|
}
|
|
|
|
public Color TintColor
|
|
{
|
|
get => (Color)GetValue(TintColorProperty);
|
|
set => SetValue(TintColorProperty, value);
|
|
}
|
|
|
|
public double TintIntensity
|
|
{
|
|
get => (double)GetValue(TintIntensityProperty);
|
|
set => SetValue(TintIntensityProperty, value);
|
|
}
|
|
|
|
private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is BlurImageControl control && control._imageBrush != null)
|
|
{
|
|
control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
|
|
}
|
|
}
|
|
|
|
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is BlurImageControl control && control._compositor != null)
|
|
{
|
|
control.UpdateEffect();
|
|
}
|
|
}
|
|
|
|
private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is BlurImageControl control && control._effectVisual != null)
|
|
{
|
|
control._effectVisual.Opacity = (float)(double)e.NewValue;
|
|
}
|
|
}
|
|
|
|
private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is BlurImageControl control && control._effectBrush != null)
|
|
{
|
|
control.UpdateEffect();
|
|
}
|
|
}
|
|
|
|
private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is BlurImageControl control && control._effectBrush != null)
|
|
{
|
|
control.UpdateEffect();
|
|
}
|
|
}
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
InitializeComposition();
|
|
}
|
|
|
|
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
|
{
|
|
if (_effectVisual != null)
|
|
{
|
|
_effectVisual.Size = new Vector2(
|
|
(float)Math.Max(1, e.NewSize.Width),
|
|
(float)Math.Max(1, e.NewSize.Height));
|
|
}
|
|
}
|
|
|
|
private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is not BlurImageControl control)
|
|
{
|
|
return;
|
|
}
|
|
|
|
control.EnsureEffect(force: true);
|
|
control.UpdateEffect();
|
|
}
|
|
|
|
private void InitializeComposition()
|
|
{
|
|
var visual = ElementCompositionPreview.GetElementVisual(this);
|
|
_compositor = visual.Compositor;
|
|
|
|
_effectVisual = _compositor.CreateSpriteVisual();
|
|
_effectVisual.Size = new Vector2(
|
|
(float)Math.Max(1, ActualWidth),
|
|
(float)Math.Max(1, ActualHeight));
|
|
_effectVisual.Opacity = (float)ImageOpacity;
|
|
|
|
ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
|
|
|
|
UpdateEffect();
|
|
}
|
|
|
|
private void EnsureEffect(bool force = false)
|
|
{
|
|
if (_compositor is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_effectBrush is not null && !force)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
|
|
|
|
// 1) Brightness via ArithmeticCompositeEffect
|
|
// We blend between the original image and either black or white,
|
|
// depending on whether we want to darken or brighten. BrightnessEffect isn't supported
|
|
// in the composition graph.
|
|
var brightnessEffect = new ArithmeticCompositeEffect
|
|
{
|
|
Name = BrightnessEffectName,
|
|
Source1 = imageSource, // original image
|
|
Source2 = new ColorSourceEffect
|
|
{
|
|
Name = BrightnessOverlayEffectName,
|
|
Color = Colors.Black, // we'll swap black/white via properties
|
|
},
|
|
|
|
MultiplyAmount = 0.0f,
|
|
Source1Amount = 1.0f, // original
|
|
Source2Amount = 0.0f, // overlay
|
|
Offset = 0.0f,
|
|
};
|
|
|
|
// 2) Blur
|
|
var blurEffect = new GaussianBlurEffect
|
|
{
|
|
Name = BlurEffectName,
|
|
BlurAmount = 0.0f,
|
|
BorderMode = EffectBorderMode.Hard,
|
|
Optimization = EffectOptimization.Balanced,
|
|
Source = brightnessEffect,
|
|
};
|
|
|
|
// 3) Tint (always in the chain; intensity via alpha)
|
|
var tintEffect = new BlendEffect
|
|
{
|
|
Name = TintBlendEffectName,
|
|
Background = blurEffect,
|
|
Foreground = new ColorSourceEffect
|
|
{
|
|
Name = TintEffectName,
|
|
Color = Colors.Transparent,
|
|
},
|
|
Mode = BlendEffectMode.Multiply,
|
|
};
|
|
|
|
var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
|
|
|
|
_effectBrush?.Dispose();
|
|
_effectBrush = effectFactory.CreateBrush();
|
|
|
|
if (ImageSource is not null)
|
|
{
|
|
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
|
LoadImageAsync(ImageSource);
|
|
_effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
|
}
|
|
else
|
|
{
|
|
_effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
|
|
}
|
|
|
|
if (_effectVisual is not null)
|
|
{
|
|
_effectVisual.Brush = _effectBrush;
|
|
}
|
|
}
|
|
|
|
private void UpdateEffect()
|
|
{
|
|
if (_compositor is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureEffect();
|
|
if (_effectBrush is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var props = _effectBrush.Properties;
|
|
|
|
// Brightness
|
|
var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
|
|
|
|
float source1Amount;
|
|
float source2Amount;
|
|
Color overlayColor;
|
|
|
|
if (b >= 0)
|
|
{
|
|
// Brighten: blend towards white
|
|
overlayColor = Colors.White;
|
|
source1Amount = 1.0f - b; // original image contribution
|
|
source2Amount = b; // white overlay contribution
|
|
}
|
|
else
|
|
{
|
|
// Darken: blend towards black
|
|
overlayColor = Colors.Black;
|
|
var t = -b; // 0..1
|
|
source1Amount = 1.0f - t; // original image
|
|
source2Amount = t; // black overlay
|
|
}
|
|
|
|
props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
|
|
props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
|
|
props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
|
|
|
|
// Blur
|
|
props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
|
|
|
|
// Tint
|
|
var tintColor = TintColor;
|
|
var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
|
|
|
|
var adjustedColor = Color.FromArgb(
|
|
(byte)(clampedIntensity * 255),
|
|
tintColor.R,
|
|
tintColor.G,
|
|
tintColor.B);
|
|
|
|
props.InsertColor(TintColorEffectProperty, adjustedColor);
|
|
}
|
|
|
|
private void LoadImageAsync(ImageSource imageSource)
|
|
{
|
|
try
|
|
{
|
|
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
|
if (_imageBrush is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
|
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
|
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
|
SetLoadedSurfaceToBrush(loadedSurface);
|
|
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
|
|
}
|
|
|
|
return;
|
|
|
|
void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
|
|
{
|
|
switch (e.Status)
|
|
{
|
|
case LoadedImageSourceLoadStatus.Success:
|
|
Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
|
|
|
|
try
|
|
{
|
|
SetLoadedSurfaceToBrush(loadedSurface);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to set surface in BlurImageControl", ex);
|
|
throw;
|
|
}
|
|
|
|
break;
|
|
case LoadedImageSourceLoadStatus.NetworkError:
|
|
case LoadedImageSourceLoadStatus.InvalidFormat:
|
|
case LoadedImageSourceLoadStatus.Other:
|
|
default:
|
|
Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
|
|
{
|
|
var surfaceBrush = _imageBrush;
|
|
if (surfaceBrush is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
surfaceBrush.Surface = loadedSurface;
|
|
surfaceBrush.Stretch = ConvertStretch(ImageStretch);
|
|
surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
|
}
|
|
|
|
private static CompositionStretch ConvertStretch(Stretch stretch)
|
|
{
|
|
return stretch switch
|
|
{
|
|
Stretch.None => CompositionStretch.None,
|
|
Stretch.Fill => CompositionStretch.Fill,
|
|
Stretch.Uniform => CompositionStretch.Uniform,
|
|
Stretch.UniformToFill => CompositionStretch.UniformToFill,
|
|
_ => CompositionStretch.UniformToFill,
|
|
};
|
|
}
|
|
|
|
private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
|
|
}
|