/ src / modules / cmdpal / Microsoft.CmdPal.UI / Controls / BlurImageControl.cs
BlurImageControl.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.Numerics;
  6  using ManagedCommon;
  7  using Microsoft.Graphics.Canvas.Effects;
  8  using Microsoft.UI;
  9  using Microsoft.UI.Composition;
 10  using Microsoft.UI.Xaml;
 11  using Microsoft.UI.Xaml.Controls;
 12  using Microsoft.UI.Xaml.Hosting;
 13  using Microsoft.UI.Xaml.Media;
 14  using Windows.UI;
 15  
 16  namespace Microsoft.CmdPal.UI.Controls;
 17  
 18  internal sealed partial class BlurImageControl : Control
 19  {
 20      private const string ImageSourceParameterName = "ImageSource";
 21  
 22      private const string BrightnessEffectName = "Brightness";
 23      private const string BrightnessOverlayEffectName = "BrightnessOverlay";
 24      private const string BlurEffectName = "Blur";
 25      private const string TintBlendEffectName = "TintBlend";
 26      private const string TintEffectName = "Tint";
 27  
 28  #pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
 29      private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
 30      private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
 31      private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
 32      private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
 33      private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
 34  #pragma warning restore CA1507
 35  
 36      private static readonly string[] AnimatableProperties = [
 37          BrightnessSource1AmountEffectProperty,
 38          BrightnessSource2AmountEffectProperty,
 39          BrightnessOverlayColorEffectProperty,
 40          BlurBlurAmountEffectProperty,
 41          TintColorEffectProperty
 42      ];
 43  
 44      public static readonly DependencyProperty ImageSourceProperty =
 45          DependencyProperty.Register(
 46              nameof(ImageSource),
 47              typeof(ImageSource),
 48              typeof(BlurImageControl),
 49              new PropertyMetadata(null, OnImageChanged));
 50  
 51      public static readonly DependencyProperty ImageStretchProperty =
 52          DependencyProperty.Register(
 53              nameof(ImageStretch),
 54              typeof(Stretch),
 55              typeof(BlurImageControl),
 56              new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
 57  
 58      public static readonly DependencyProperty ImageOpacityProperty =
 59          DependencyProperty.Register(
 60              nameof(ImageOpacity),
 61              typeof(double),
 62              typeof(BlurImageControl),
 63              new PropertyMetadata(1.0, OnOpacityChanged));
 64  
 65      public static readonly DependencyProperty ImageBrightnessProperty =
 66          DependencyProperty.Register(
 67              nameof(ImageBrightness),
 68              typeof(double),
 69              typeof(BlurImageControl),
 70              new PropertyMetadata(1.0, OnBrightnessChanged));
 71  
 72      public static readonly DependencyProperty BlurAmountProperty =
 73          DependencyProperty.Register(
 74              nameof(BlurAmount),
 75              typeof(double),
 76              typeof(BlurImageControl),
 77              new PropertyMetadata(0.0, OnBlurAmountChanged));
 78  
 79      public static readonly DependencyProperty TintColorProperty =
 80          DependencyProperty.Register(
 81              nameof(TintColor),
 82              typeof(Color),
 83              typeof(BlurImageControl),
 84              new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
 85  
 86      public static readonly DependencyProperty TintIntensityProperty =
 87          DependencyProperty.Register(
 88              nameof(TintIntensity),
 89              typeof(double),
 90              typeof(BlurImageControl),
 91              new PropertyMetadata(0.0, OnVisualPropertyChanged));
 92  
 93      private Compositor? _compositor;
 94      private SpriteVisual? _effectVisual;
 95      private CompositionEffectBrush? _effectBrush;
 96      private CompositionSurfaceBrush? _imageBrush;
 97  
 98      public BlurImageControl()
 99      {
100          this.DefaultStyleKey = typeof(BlurImageControl);
101          this.Loaded += OnLoaded;
102          this.SizeChanged += OnSizeChanged;
103      }
104  
105      public ImageSource ImageSource
106      {
107          get => (ImageSource)GetValue(ImageSourceProperty);
108          set => SetValue(ImageSourceProperty, value);
109      }
110  
111      public Stretch ImageStretch
112      {
113          get => (Stretch)GetValue(ImageStretchProperty);
114          set => SetValue(ImageStretchProperty, value);
115      }
116  
117      public double ImageOpacity
118      {
119          get => (double)GetValue(ImageOpacityProperty);
120          set => SetValue(ImageOpacityProperty, value);
121      }
122  
123      public double ImageBrightness
124      {
125          get => (double)GetValue(ImageBrightnessProperty);
126          set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
127      }
128  
129      public double BlurAmount
130      {
131          get => (double)GetValue(BlurAmountProperty);
132          set => SetValue(BlurAmountProperty, value);
133      }
134  
135      public Color TintColor
136      {
137          get => (Color)GetValue(TintColorProperty);
138          set => SetValue(TintColorProperty, value);
139      }
140  
141      public double TintIntensity
142      {
143          get => (double)GetValue(TintIntensityProperty);
144          set => SetValue(TintIntensityProperty, value);
145      }
146  
147      private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
148      {
149          if (d is BlurImageControl control && control._imageBrush != null)
150          {
151              control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
152          }
153      }
154  
155      private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
156      {
157          if (d is BlurImageControl control && control._compositor != null)
158          {
159              control.UpdateEffect();
160          }
161      }
162  
163      private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
164      {
165          if (d is BlurImageControl control && control._effectVisual != null)
166          {
167              control._effectVisual.Opacity = (float)(double)e.NewValue;
168          }
169      }
170  
171      private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
172      {
173          if (d is BlurImageControl control && control._effectBrush != null)
174          {
175              control.UpdateEffect();
176          }
177      }
178  
179      private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
180      {
181          if (d is BlurImageControl control && control._effectBrush != null)
182          {
183              control.UpdateEffect();
184          }
185      }
186  
187      private void OnLoaded(object sender, RoutedEventArgs e)
188      {
189          InitializeComposition();
190      }
191  
192      private void OnSizeChanged(object sender, SizeChangedEventArgs e)
193      {
194          if (_effectVisual != null)
195          {
196              _effectVisual.Size = new Vector2(
197                  (float)Math.Max(1, e.NewSize.Width),
198                  (float)Math.Max(1, e.NewSize.Height));
199          }
200      }
201  
202      private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
203      {
204          if (d is not BlurImageControl control)
205          {
206              return;
207          }
208  
209          control.EnsureEffect(force: true);
210          control.UpdateEffect();
211      }
212  
213      private void InitializeComposition()
214      {
215          var visual = ElementCompositionPreview.GetElementVisual(this);
216          _compositor = visual.Compositor;
217  
218          _effectVisual = _compositor.CreateSpriteVisual();
219          _effectVisual.Size = new Vector2(
220              (float)Math.Max(1, ActualWidth),
221              (float)Math.Max(1, ActualHeight));
222          _effectVisual.Opacity = (float)ImageOpacity;
223  
224          ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
225  
226          UpdateEffect();
227      }
228  
229      private void EnsureEffect(bool force = false)
230      {
231          if (_compositor is null)
232          {
233              return;
234          }
235  
236          if (_effectBrush is not null && !force)
237          {
238              return;
239          }
240  
241          var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
242  
243          // 1) Brightness via ArithmeticCompositeEffect
244          // We blend between the original image and either black or white,
245          // depending on whether we want to darken or brighten. BrightnessEffect isn't supported
246          // in the composition graph.
247          var brightnessEffect = new ArithmeticCompositeEffect
248          {
249              Name = BrightnessEffectName,
250              Source1 = imageSource, // original image
251              Source2 = new ColorSourceEffect
252              {
253                  Name = BrightnessOverlayEffectName,
254                  Color = Colors.Black, // we'll swap black/white via properties
255              },
256  
257              MultiplyAmount = 0.0f,
258              Source1Amount = 1.0f, // original
259              Source2Amount = 0.0f, // overlay
260              Offset = 0.0f,
261          };
262  
263          // 2) Blur
264          var blurEffect = new GaussianBlurEffect
265          {
266              Name = BlurEffectName,
267              BlurAmount = 0.0f,
268              BorderMode = EffectBorderMode.Hard,
269              Optimization = EffectOptimization.Balanced,
270              Source = brightnessEffect,
271          };
272  
273          // 3) Tint (always in the chain; intensity via alpha)
274          var tintEffect = new BlendEffect
275          {
276              Name = TintBlendEffectName,
277              Background = blurEffect,
278              Foreground = new ColorSourceEffect
279              {
280                  Name = TintEffectName,
281                  Color = Colors.Transparent,
282              },
283              Mode = BlendEffectMode.Multiply,
284          };
285  
286          var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
287  
288          _effectBrush?.Dispose();
289          _effectBrush = effectFactory.CreateBrush();
290  
291          // Set initial source
292          if (ImageSource is not null)
293          {
294              _imageBrush ??= _compositor.CreateSurfaceBrush();
295              LoadImageAsync(ImageSource);
296              _effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
297          }
298          else
299          {
300              _effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
301          }
302  
303          if (_effectVisual is not null)
304          {
305              _effectVisual.Brush = _effectBrush;
306          }
307      }
308  
309      private void UpdateEffect()
310      {
311          if (_compositor is null)
312          {
313              return;
314          }
315  
316          EnsureEffect();
317          if (_effectBrush is null)
318          {
319              return;
320          }
321  
322          var props = _effectBrush.Properties;
323  
324          // Brightness
325          var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
326  
327          float source1Amount;
328          float source2Amount;
329          Color overlayColor;
330  
331          if (b >= 0)
332          {
333              // Brighten: blend towards white
334              overlayColor = Colors.White;
335              source1Amount = 1.0f - b; // original image contribution
336              source2Amount = b;        // white overlay contribution
337          }
338          else
339          {
340              // Darken: blend towards black
341              overlayColor = Colors.Black;
342              var t = -b;               // 0..1
343              source1Amount = 1.0f - t; // original image
344              source2Amount = t;        // black overlay
345          }
346  
347          props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
348          props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
349          props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
350  
351          // Blur
352          props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
353  
354          // Tint
355          var tintColor = TintColor;
356          var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
357  
358          var adjustedColor = Color.FromArgb(
359              (byte)(clampedIntensity * 255),
360              tintColor.R,
361              tintColor.G,
362              tintColor.B);
363  
364          props.InsertColor(TintColorEffectProperty, adjustedColor);
365      }
366  
367      private void LoadImageAsync(ImageSource imageSource)
368      {
369          try
370          {
371              if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
372              {
373                  return;
374              }
375  
376              _imageBrush ??= _compositor?.CreateSurfaceBrush();
377              if (_imageBrush is null)
378              {
379                  return;
380              }
381  
382              Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
383              var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
384              loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
385              SetLoadedSurfaceToBrush(loadedSurface);
386              _effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
387          }
388          catch (Exception ex)
389          {
390              Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
391          }
392  
393          return;
394  
395          void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
396          {
397              switch (e.Status)
398              {
399                  case LoadedImageSourceLoadStatus.Success:
400                      Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
401  
402                      try
403                      {
404                          SetLoadedSurfaceToBrush(loadedSurface);
405                      }
406                      catch (Exception ex)
407                      {
408                          Logger.LogError("Failed to set surface in BlurImageControl", ex);
409                          throw;
410                      }
411  
412                      break;
413                  case LoadedImageSourceLoadStatus.NetworkError:
414                  case LoadedImageSourceLoadStatus.InvalidFormat:
415                  case LoadedImageSourceLoadStatus.Other:
416                  default:
417                      Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
418                      break;
419              }
420          }
421      }
422  
423      private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
424      {
425          var surfaceBrush = _imageBrush;
426          if (surfaceBrush is null)
427          {
428              return;
429          }
430  
431          surfaceBrush.Surface = loadedSurface;
432          surfaceBrush.Stretch = ConvertStretch(ImageStretch);
433          surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
434      }
435  
436      private static CompositionStretch ConvertStretch(Stretch stretch)
437      {
438          return stretch switch
439          {
440              Stretch.None => CompositionStretch.None,
441              Stretch.Fill => CompositionStretch.Fill,
442              Stretch.Uniform => CompositionStretch.Uniform,
443              Stretch.UniformToFill => CompositionStretch.UniformToFill,
444              _ => CompositionStretch.UniformToFill,
445          };
446      }
447  
448      private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
449  }