From 09f61d896e6185b5bd980cbe98a9ebe941fe39cc Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 12 Apr 2023 23:14:19 -0700 Subject: [PATCH 1/7] Initial Raw Port into New Experiment --- components/Effects/OpenSolution.bat | 3 + components/Effects/samples/Dependencies.props | 31 ++ .../Effects/samples/Effects.Samples.csproj | 8 + components/Effects/samples/Effects.md | 47 +++ .../samples/EffectsTemplatedSample.xaml | 16 + .../samples/EffectsTemplatedSample.xaml.cs | 21 + .../Effects/src/AdditionalAssemblyInfo.cs | 13 + .../src/CommunityToolkit.WinUI.Effects.csproj | 13 + components/Effects/src/Dependencies.props | 31 ++ components/Effects/src/MultiTarget.props | 10 + .../Effects/src/Shadows/AttachedDropShadow.cs | 371 ++++++++++++++++++ .../Effects/src/Shadows/AttachedShadowBase.cs | 276 +++++++++++++ .../Shadows/AttachedShadowElementContext.cs | 320 +++++++++++++++ components/Effects/src/Shadows/Effects.cs | 58 +++ .../Effects/src/Shadows/IAlphaMaskProvider.cs | 25 ++ .../Effects/src/Shadows/IAttachedShadow.cs | 49 +++ .../Effects/src/Shadows/TypedResourceKey.cs | 35 ++ .../Effects/tests/Effects.Tests.projitems | 23 ++ components/Effects/tests/Effects.Tests.shproj | 13 + .../Effects/tests/ExampleEffectsTestClass.cs | 133 +++++++ .../Effects/tests/ExampleEffectsTestPage.xaml | 14 + .../tests/ExampleEffectsTestPage.xaml.cs | 16 + 22 files changed, 1526 insertions(+) create mode 100644 components/Effects/OpenSolution.bat create mode 100644 components/Effects/samples/Dependencies.props create mode 100644 components/Effects/samples/Effects.Samples.csproj create mode 100644 components/Effects/samples/Effects.md create mode 100644 components/Effects/samples/EffectsTemplatedSample.xaml create mode 100644 components/Effects/samples/EffectsTemplatedSample.xaml.cs create mode 100644 components/Effects/src/AdditionalAssemblyInfo.cs create mode 100644 components/Effects/src/CommunityToolkit.WinUI.Effects.csproj create mode 100644 components/Effects/src/Dependencies.props create mode 100644 components/Effects/src/MultiTarget.props create mode 100644 components/Effects/src/Shadows/AttachedDropShadow.cs create mode 100644 components/Effects/src/Shadows/AttachedShadowBase.cs create mode 100644 components/Effects/src/Shadows/AttachedShadowElementContext.cs create mode 100644 components/Effects/src/Shadows/Effects.cs create mode 100644 components/Effects/src/Shadows/IAlphaMaskProvider.cs create mode 100644 components/Effects/src/Shadows/IAttachedShadow.cs create mode 100644 components/Effects/src/Shadows/TypedResourceKey.cs create mode 100644 components/Effects/tests/Effects.Tests.projitems create mode 100644 components/Effects/tests/Effects.Tests.shproj create mode 100644 components/Effects/tests/ExampleEffectsTestClass.cs create mode 100644 components/Effects/tests/ExampleEffectsTestPage.xaml create mode 100644 components/Effects/tests/ExampleEffectsTestPage.xaml.cs diff --git a/components/Effects/OpenSolution.bat b/components/Effects/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Effects/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Effects/samples/Dependencies.props b/components/Effects/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Effects/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Effects/samples/Effects.Samples.csproj b/components/Effects/samples/Effects.Samples.csproj new file mode 100644 index 00000000..c6a01c3f --- /dev/null +++ b/components/Effects/samples/Effects.Samples.csproj @@ -0,0 +1,8 @@ + + + Effects + + + + + diff --git a/components/Effects/samples/Effects.md b/components/Effects/samples/Effects.md new file mode 100644 index 00000000..69a95b29 --- /dev/null +++ b/components/Effects/samples/Effects.md @@ -0,0 +1,47 @@ +--- +title: Effects +author: githubaccount +description: TODO: Your experiment's description here +keywords: Effects, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +--- + + + + + + + + + +# Effects + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample EffectsCustomSample] + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. +Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` +classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. + +The `_ClassicBinding` class shows the traditional method used to develop components with best practices. + +### Implict style + +> [!SAMPLE EffectsTemplatedSample] diff --git a/components/Effects/samples/EffectsTemplatedSample.xaml b/components/Effects/samples/EffectsTemplatedSample.xaml new file mode 100644 index 00000000..bdb6a03c --- /dev/null +++ b/components/Effects/samples/EffectsTemplatedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/Effects/samples/EffectsTemplatedSample.xaml.cs b/components/Effects/samples/EffectsTemplatedSample.xaml.cs new file mode 100644 index 00000000..e7d57681 --- /dev/null +++ b/components/Effects/samples/EffectsTemplatedSample.xaml.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace EffectsExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(EffectsTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] +public sealed partial class EffectsTemplatedSample : Page +{ + public EffectsTemplatedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Effects/src/AdditionalAssemblyInfo.cs b/components/Effects/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..176aff82 --- /dev/null +++ b/components/Effects/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("Effects.Tests.Uwp")] +[assembly: InternalsVisibleTo("Effects.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Effects/src/CommunityToolkit.WinUI.Effects.csproj b/components/Effects/src/CommunityToolkit.WinUI.Effects.csproj new file mode 100644 index 00000000..9d2b1300 --- /dev/null +++ b/components/Effects/src/CommunityToolkit.WinUI.Effects.csproj @@ -0,0 +1,13 @@ + + + Effects + This package contains Effects. + 8.0.0-beta.1 + + + CommunityToolkit.WinUI.EffectsRns + + + + + diff --git a/components/Effects/src/Dependencies.props b/components/Effects/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Effects/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Effects/src/MultiTarget.props b/components/Effects/src/MultiTarget.props new file mode 100644 index 00000000..ca9bfd7b --- /dev/null +++ b/components/Effects/src/MultiTarget.props @@ -0,0 +1,10 @@ + + + + + uwp;wasdk; + + diff --git a/components/Effects/src/Shadows/AttachedDropShadow.cs b/components/Effects/src/Shadows/AttachedDropShadow.cs new file mode 100644 index 00000000..b2512fdb --- /dev/null +++ b/components/Effects/src/Shadows/AttachedDropShadow.cs @@ -0,0 +1,371 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Numerics; +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.Shapes; +using Windows.Foundation; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// A helper to add a composition based drop shadow to a . + /// + public sealed class AttachedDropShadow : AttachedShadowBase + { + private const float MaxBlurRadius = 72; + + /// + protected internal override bool SupportsOnSizeChangedEvent => true; + + private static readonly TypedResourceKey RoundedRectangleGeometryResourceKey = "RoundedGeometry"; + private static readonly TypedResourceKey ShapeResourceKey = "Shape"; + private static readonly TypedResourceKey ShapeVisualResourceKey = "ShapeVisual"; + private static readonly TypedResourceKey SurfaceBrushResourceKey = "SurfaceBrush"; + private static readonly TypedResourceKey VisualSurfaceResourceKey = "VisualSurface"; + + /// + /// Gets or sets a value indicating whether the panel uses an alpha mask to create a more precise shadow vs. a quicker rectangle shape. + /// + /// + /// Turn this off to lose fidelity and gain performance of the panel. + /// + public bool IsMasked + { + get { return (bool)GetValue(IsMaskedProperty); } + set { SetValue(IsMaskedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsMaskedProperty = + DependencyProperty.Register(nameof(IsMasked), typeof(bool), typeof(AttachedDropShadow), new PropertyMetadata(true, OnDependencyPropertyChanged)); + + /// + /// Gets or sets the roundness of the shadow's corners. + /// + public double CornerRadius + { + get => (double)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// The for + /// + public static readonly DependencyProperty CornerRadiusProperty = + DependencyProperty.Register( + nameof(CornerRadius), + typeof(double), + typeof(AttachedDropShadow), + new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4 + + /// + /// Gets or sets the to be used as a backdrop to cast shadows on. + /// + public FrameworkElement CastTo + { + get { return (FrameworkElement)GetValue(CastToProperty); } + set { SetValue(CastToProperty, value); } + } + + /// + /// The for + /// + public static readonly DependencyProperty CastToProperty = + DependencyProperty.Register(nameof(CastTo), typeof(FrameworkElement), typeof(AttachedDropShadow), new PropertyMetadata(null, OnCastToPropertyChanged)); // TODO: Property Change + + private ContainerVisual _container; + + private static void OnCastToPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AttachedDropShadow shadow) + { + if (e.OldValue is FrameworkElement element) + { + ElementCompositionPreview.SetElementChildVisual(element, null); + element.SizeChanged -= shadow.CastToElement_SizeChanged; + } + + if (e.NewValue is FrameworkElement elementNew) + { + var prevContainer = shadow._container; + + var child = ElementCompositionPreview.GetElementChildVisual(elementNew); + if (child is ContainerVisual visual) + { + shadow._container = visual; + } + else + { + var compositor = ElementCompositionPreview.GetElementVisual(shadow.CastTo).Compositor; + shadow._container = compositor.CreateContainerVisual(); + + ElementCompositionPreview.SetElementChildVisual(elementNew, shadow._container); + } + + // Need to remove all old children from previous container if it's changed + if (prevContainer != null && prevContainer != shadow._container) + { + foreach (var context in shadow.EnumerateElementContexts()) + { + if (context.IsInitialized && + prevContainer.Children.Contains(context.SpriteVisual)) + { + prevContainer.Children.Remove(context.SpriteVisual); + } + } + } + + // Make sure all child shadows are hooked into container + foreach (var context in shadow.EnumerateElementContexts()) + { + if (context.IsInitialized) + { + shadow.SetElementChildVisual(context); + } + } + + elementNew.SizeChanged += shadow.CastToElement_SizeChanged; + + // Re-trigger updates to all shadow locations for new parent + shadow.CastToElement_SizeChanged(null, null); + } + } + } + + private void CastToElement_SizeChanged(object sender, SizeChangedEventArgs e) + { + // Don't use sender or 'e' here as related to container element not + // element for shadow, grab values off context. (Also may be null from internal call.) + foreach (var context in EnumerateElementContexts()) + { + if (context.IsInitialized) + { + // TODO: Should we use ActualWidth/Height instead of RenderSize? + OnSizeChanged(context, context.Element.RenderSize, context.Element.RenderSize); + } + } + } + + /// + protected internal override void OnElementContextUninitialized(AttachedShadowElementContext context) + { + if (_container != null && _container.Children.Contains(context.SpriteVisual)) + { + _container.Children.Remove(context.SpriteVisual); + } + + context.SpriteVisual?.StopAnimation("Size"); + + context.Element.LayoutUpdated -= Element_LayoutUpdated; + + if (context.VisibilityToken != null) + { + context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); + context.VisibilityToken = null; + } + + base.OnElementContextUninitialized(context); + } + + /// + protected override void SetElementChildVisual(AttachedShadowElementContext context) + { + if (_container != null && !_container.Children.Contains(context.SpriteVisual)) + { + _container.Children.InsertAtTop(context.SpriteVisual); + } + + // Handles size changing and other elements around it updating. + context.Element.LayoutUpdated -= Element_LayoutUpdated; + context.Element.LayoutUpdated += Element_LayoutUpdated; + + if (context.VisibilityToken != null) + { + context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); + context.VisibilityToken = null; + } + + context.VisibilityToken = context.Element.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, Element_VisibilityChanged); + } + + private void Element_LayoutUpdated(object sender, object e) + { + // Update other shadows to account for layout changes + CastToElement_SizeChanged(null, null); + } + + private void Element_VisibilityChanged(DependencyObject sender, DependencyProperty dp) + { + if (sender is FrameworkElement element) + { + var context = GetElementContext(element); + + if (element.Visibility == Visibility.Collapsed) + { + if (_container != null && _container.Children.Contains(context.SpriteVisual)) + { + _container.Children.Remove(context.SpriteVisual); + } + } + else + { + if (_container != null && !_container.Children.Contains(context.SpriteVisual)) + { + _container.Children.InsertAtTop(context.SpriteVisual); + } + } + } + + // Update other shadows to account for layout changes + CastToElement_SizeChanged(null, null); + } + + /// + protected override CompositionBrush GetShadowMask(AttachedShadowElementContext context) + { + CompositionBrush mask = null; + + if (DesignTimeHelpers.IsRunningInLegacyDesignerMode) + { + return null; + } + + if (context.Element != null) + { + if (IsMasked) + { + // We check for IAlphaMaskProvider first, to ensure that we use the custom + // alpha mask even if Content happens to extend any of the other classes + if (context.Element is IAlphaMaskProvider maskedControl) + { + if (maskedControl.WaitUntilLoaded && !context.Element.IsLoaded) + { + context.Element.Loaded += CustomMaskedElement_Loaded; + } + else + { + mask = maskedControl.GetAlphaMask(); + } + } + else if (context.Element is Image) + { + mask = ((Image)context.Element).GetAlphaMask(); + } + else if (context.Element is Shape) + { + mask = ((Shape)context.Element).GetAlphaMask(); + } + else if (context.Element is TextBlock) + { + mask = ((TextBlock)context.Element).GetAlphaMask(); + } + } + + // If we don't have a mask and have specified rounded corners, we'll generate a simple quick mask. + // This is the same code from link:AttachedCardShadow.cs:GetShadowMask + if (mask == null && CornerRadius > 0) + { + // Create rounded rectangle geometry and add it to a shape + var geometry = context.GetResource(RoundedRectangleGeometryResourceKey) ?? context.AddResource( + RoundedRectangleGeometryResourceKey, + context.Compositor.CreateRoundedRectangleGeometry()); + geometry.CornerRadius = new Vector2((float)CornerRadius); + + var shape = context.GetResource(ShapeResourceKey) ?? context.AddResource(ShapeResourceKey, context.Compositor.CreateSpriteShape(geometry)); + shape.FillBrush = context.Compositor.CreateColorBrush(Colors.Black); + + // Create a ShapeVisual so that our geometry can be rendered to a visual + var shapeVisual = context.GetResource(ShapeVisualResourceKey) ?? + context.AddResource(ShapeVisualResourceKey, context.Compositor.CreateShapeVisual()); + shapeVisual.Shapes.Add(shape); + + // Create a CompositionVisualSurface, which renders our ShapeVisual to a texture + var visualSurface = context.GetResource(VisualSurfaceResourceKey) ?? + context.AddResource(VisualSurfaceResourceKey, context.Compositor.CreateVisualSurface()); + visualSurface.SourceVisual = shapeVisual; + + // Create a CompositionSurfaceBrush to render our CompositionVisualSurface to a brush. + // Now we have a rounded rectangle brush that can be used on as the mask for our shadow. + var surfaceBrush = context.GetResource(SurfaceBrushResourceKey) ?? context.AddResource( + SurfaceBrushResourceKey, + context.Compositor.CreateSurfaceBrush(visualSurface)); + + geometry.Size = visualSurface.SourceSize = shapeVisual.Size = context.Element.RenderSize.ToVector2(); + + mask = surfaceBrush; + } + } + + // Position our shadow in the correct spot to match the corresponding element. + context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); + + BindSizeAndScale(context.SpriteVisual, context.Element); + + return mask; + } + + private static void BindSizeAndScale(CompositionObject source, UIElement target) + { + var visual = ElementCompositionPreview.GetElementVisual(target); + var bindSizeAnimation = source.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size * {nameof(visual)}.Scale.XY"); + + bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); + + // Start the animation + source.StartAnimation("Size", bindSizeAnimation); + } + + private void CustomMaskedElement_Loaded(object sender, RoutedEventArgs e) + { + var context = GetElementContext(sender as FrameworkElement); + + context.Element.Loaded -= CustomMaskedElement_Loaded; + + UpdateShadowClip(context); + UpdateShadowMask(context); + } + + /// + protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) + { + context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); + + UpdateShadowClip(context); + + base.OnSizeChanged(context, newSize, previousSize); + } + + /// + protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + { + if (property == IsMaskedProperty) + { + UpdateShadowMask(context); + } + else if (property == CornerRadiusProperty) + { + var geometry = context.GetResource(RoundedRectangleGeometryResourceKey); + if (geometry != null) + { + geometry.CornerRadius = new Vector2((float)(double)newValue); + } + + UpdateShadowMask(context); + } + else + { + base.OnPropertyChanged(context, property, oldValue, newValue); + } + } + } +} diff --git a/components/Effects/src/Shadows/AttachedShadowBase.cs b/components/Effects/src/Shadows/AttachedShadowBase.cs new file mode 100644 index 00000000..9794f0b8 --- /dev/null +++ b/components/Effects/src/Shadows/AttachedShadowBase.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation 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 System.Numerics; +using System.Runtime.CompilerServices; +using Microsoft.UI; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Hosting; +using Windows.Foundation; +using Windows.Foundation.Metadata; +using Windows.UI; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// The base class for attached shadows. + /// + public abstract class AttachedShadowBase : DependencyObject, IAttachedShadow + { + /// + /// The for . + /// + public static readonly DependencyProperty BlurRadiusProperty = + DependencyProperty.Register(nameof(BlurRadius), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(12d, OnDependencyPropertyChanged)); + + /// + /// The for . + /// + public static readonly DependencyProperty ColorProperty = + DependencyProperty.Register(nameof(Color), typeof(Color), typeof(AttachedShadowBase), new PropertyMetadata(Colors.Black, OnDependencyPropertyChanged)); + + /// + /// The for . + /// + public static readonly DependencyProperty OffsetProperty = + DependencyProperty.Register( + nameof(Offset), + typeof(string), // Needs to be string as we can't convert in XAML natively from Vector3, see https://github.com/microsoft/microsoft-ui-xaml/issues/3896 + typeof(AttachedShadowBase), + new PropertyMetadata(string.Empty, OnDependencyPropertyChanged)); + + /// + /// The for + /// + public static readonly DependencyProperty OpacityProperty = + DependencyProperty.Register(nameof(Opacity), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(1d, OnDependencyPropertyChanged)); + + /// + /// Gets or sets the collection of for each element this is connected to. + /// + private ConditionalWeakTable ShadowElementContextTable { get; set; } + + /// + public double BlurRadius + { + get => (double)GetValue(BlurRadiusProperty); + set => SetValue(BlurRadiusProperty, value); + } + + /// + public double Opacity + { + get => (double)GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + /// + public string Offset + { + get => (string)GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } + + /// + public Color Color + { + get => (Color)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Gets a value indicating whether or not OnSizeChanged should be called when is fired. + /// + protected internal abstract bool SupportsOnSizeChangedEvent { get; } + + /// + /// Use this method as the for DependencyProperties in derived classes. + /// + protected static void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + (sender as AttachedShadowBase)?.CallPropertyChangedForEachElement(args.Property, args.OldValue, args.NewValue); + } + + internal void ConnectElement(FrameworkElement element) + { + ShadowElementContextTable = ShadowElementContextTable ?? new ConditionalWeakTable(); + if (ShadowElementContextTable.TryGetValue(element, out var context)) + { + return; + } + + context = new AttachedShadowElementContext(); + context.ConnectToElement(this, element); + ShadowElementContextTable.Add(element, context); + } + + internal void DisconnectElement(FrameworkElement element) + { + if (ShadowElementContextTable == null) + { + return; + } + + if (ShadowElementContextTable.TryGetValue(element, out var context)) + { + context.DisconnectFromElement(); + ShadowElementContextTable.Remove(element); + } + } + + /// + /// Override to handle when the for an element is being initialized. + /// + /// The that is being initialized. + protected internal virtual void OnElementContextInitialized(AttachedShadowElementContext context) + { + OnPropertyChanged(context, OpacityProperty, Opacity, Opacity); + OnPropertyChanged(context, BlurRadiusProperty, BlurRadius, BlurRadius); + OnPropertyChanged(context, ColorProperty, Color, Color); + OnPropertyChanged(context, OffsetProperty, Offset, Offset); + UpdateShadowClip(context); + UpdateShadowMask(context); + SetElementChildVisual(context); + } + + /// + /// Override to handle when the for an element is being uninitialized. + /// + /// The that is being uninitialized. + protected internal virtual void OnElementContextUninitialized(AttachedShadowElementContext context) + { + context.ClearAndDisposeResources(); + ElementCompositionPreview.SetElementChildVisual(context.Element, null); + } + + /// + public AttachedShadowElementContext GetElementContext(FrameworkElement element) + { + if (ShadowElementContextTable != null && ShadowElementContextTable.TryGetValue(element, out var context)) + { + return context; + } + + return null; + } + + /// + public IEnumerable EnumerateElementContexts() + { + foreach (var kvp in ShadowElementContextTable) + { + yield return kvp.Value; + } + } + + /// + /// Sets as a child visual on + /// + /// The this operaiton will be performed on. + protected virtual void SetElementChildVisual(AttachedShadowElementContext context) + { + ElementCompositionPreview.SetElementChildVisual(context.Element, context.SpriteVisual); + } + + private void CallPropertyChangedForEachElement(DependencyProperty property, object oldValue, object newValue) + { + if (ShadowElementContextTable == null) + { + return; + } + + foreach (var context in ShadowElementContextTable) + { + if (context.Value.IsInitialized) + { + OnPropertyChanged(context.Value, property, oldValue, newValue); + } + } + } + + /// + /// Get a in the shape of the element that is casting the shadow. + /// + /// A representing the shape of an element. + protected virtual CompositionBrush GetShadowMask(AttachedShadowElementContext context) + { + return null; + } + + /// + /// Get the for the shadow's + /// + /// A for the extent of the shadowed area. + protected virtual CompositionClip GetShadowClip(AttachedShadowElementContext context) + { + return null; + } + + /// + /// Update the mask that gives the shadow its shape. + /// + protected void UpdateShadowMask(AttachedShadowElementContext context) + { + if (!context.IsInitialized) + { + return; + } + + context.Shadow.Mask = GetShadowMask(context); + } + + /// + /// Update the clipping on the shadow's . + /// + protected void UpdateShadowClip(AttachedShadowElementContext context) + { + if (!context.IsInitialized) + { + return; + } + + context.SpriteVisual.Clip = GetShadowClip(context); + } + + /// + /// This method is called when a DependencyProperty is changed. + /// + protected virtual void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + { + if (!context.IsInitialized) + { + return; + } + + if (property == BlurRadiusProperty) + { + context.Shadow.BlurRadius = (float)(double)newValue; + } + else if (property == OpacityProperty) + { + context.Shadow.Opacity = (float)(double)newValue; + } + else if (property == ColorProperty) + { + context.Shadow.Color = (Color)newValue; + } + else if (property == OffsetProperty) + { + context.Shadow.Offset = (Vector3)(newValue as string)?.ToVector3(); + } + } + + /// + /// This method is called when the element size changes, and = true. + /// + /// The for the firing its SizeChanged event + /// The new size of the + /// The previous size of the + protected internal virtual void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) + { + } + } +} \ No newline at end of file diff --git a/components/Effects/src/Shadows/AttachedShadowElementContext.cs b/components/Effects/src/Shadows/AttachedShadowElementContext.cs new file mode 100644 index 00000000..75cf79d0 --- /dev/null +++ b/components/Effects/src/Shadows/AttachedShadowElementContext.cs @@ -0,0 +1,320 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Numerics; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Hosting; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// Class which maintains the context of a for a particular linked to the definition of that shadow provided by the implementation being used. + /// + public sealed class AttachedShadowElementContext + { + private bool _isConnected; + + private Dictionary _resources; + + internal long? VisibilityToken { get; set; } + + /// + /// Gets a value indicating whether or not this has been initialized. + /// + public bool IsInitialized { get; private set; } + + /// + /// Gets the that contains this . + /// + public AttachedShadowBase Parent { get; private set; } + + /// + /// Gets the this instance is attached to + /// + public FrameworkElement Element { get; private set; } + + /// + /// Gets the for the this instance is attached to. + /// + public Visual ElementVisual { get; private set; } + + /// + /// Gets the for this instance. + /// + public Compositor Compositor { get; private set; } + + /// + /// Gets the that contains the shadow for this instance + /// + public SpriteVisual SpriteVisual { get; private set; } + + /// + /// Gets the that is rendered on this instance's + /// + public DropShadow Shadow { get; private set; } + + /// + /// Connects a to its parent definition. + /// + /// The that is using this context. + /// The that a shadow is being attached to. + internal void ConnectToElement(AttachedShadowBase parent, FrameworkElement element) + { + if (_isConnected) + { + throw new InvalidOperationException("This AttachedShadowElementContext has already been connected to an element"); + } + + _isConnected = true; + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + Element = element ?? throw new ArgumentNullException(nameof(element)); + Element.Loaded += OnElementLoaded; + Element.Unloaded += OnElementUnloaded; + Initialize(); + } + + internal void DisconnectFromElement() + { + if (!_isConnected) + { + return; + } + + Uninitialize(); + + Element.Loaded -= OnElementLoaded; + Element.Unloaded -= OnElementUnloaded; + Element = null; + + Parent = null; + + _isConnected = false; + } + + /// + /// Force early creation of this instance's resources, otherwise they will be created automatically when is loaded. + /// + public void CreateResources() => Initialize(true); + + private void Initialize(bool forceIfNotLoaded = false) + { + if (IsInitialized || !_isConnected || (!Element.IsLoaded && !forceIfNotLoaded)) + { + return; + } + + IsInitialized = true; + + ElementVisual = ElementCompositionPreview.GetElementVisual(Element); + Compositor = ElementVisual.Compositor; + + Shadow = Compositor.CreateDropShadow(); + + SpriteVisual = Compositor.CreateSpriteVisual(); + SpriteVisual.RelativeSizeAdjustment = Vector2.One; + SpriteVisual.Shadow = Shadow; + + if (Parent.SupportsOnSizeChangedEvent) + { + Element.SizeChanged += OnElementSizeChanged; + } + + Parent?.OnElementContextInitialized(this); + } + + private void Uninitialize() + { + if (!IsInitialized) + { + return; + } + + IsInitialized = false; + + Parent.OnElementContextUninitialized(this); + + SpriteVisual.Shadow = null; + SpriteVisual.Dispose(); + + Shadow.Dispose(); + + ElementCompositionPreview.SetElementChildVisual(Element, null); + + Element.SizeChanged -= OnElementSizeChanged; + + SpriteVisual = null; + Shadow = null; + ElementVisual = null; + } + + private void OnElementUnloaded(object sender, RoutedEventArgs e) + { + Uninitialize(); + } + + private void OnElementLoaded(object sender, RoutedEventArgs e) + { + Initialize(); + } + + private void OnElementSizeChanged(object sender, SizeChangedEventArgs e) + { + Parent?.OnSizeChanged(this, e.NewSize, e.PreviousSize); + } + + /// + /// Adds a resource to this instance's resource dictionary with the specified key + /// + /// The type of the resource being added. + /// Key to use to lookup the resource later. + /// Object to store within the resource dictionary. + /// The added resource + public T AddResource(string key, T resource) + { + _resources = _resources ?? new Dictionary(); + if (_resources.ContainsKey(key)) + { + _resources[key] = resource; + } + else + { + _resources.Add(key, resource); + } + + return resource; + } + + /// + /// Retrieves a resource with the specified key and type if it exists + /// + /// The type of the resource being retrieved. + /// Key to use to lookup the resource. + /// Object to retrieved from the resource dictionary or default value. + /// True if the resource exists, false otherwise + public bool TryGetResource(string key, out T resource) + { + if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource) + { + resource = tResource; + return true; + } + + resource = default; + return false; + } + + /// + /// Retries a resource with the specified key and type + /// + /// The type of the resource being retrieved. + /// Key to use to lookup the resource. + /// The resource if available, otherwise default value. + public T GetResource(string key) + { + if (TryGetResource(key, out T resource)) + { + return resource; + } + + return default; + } + + /// + /// Removes an existing resource with the specified key and type + /// + /// The type of the resource being removed. + /// Key to use to lookup the resource. + /// The resource that was removed, if any + public T RemoveResource(string key) + { + if (_resources.TryGetValue(key, out var objResource)) + { + _resources.Remove(key); + if (objResource is T resource) + { + return resource; + } + } + + return default; + } + + /// + /// Removes an existing resource with the specified key and type, and disposes it + /// + /// The type of the resource being removed. + /// Key to use to lookup the resource. + /// The resource that was removed, if any + public T RemoveAndDisposeResource(string key) + where T : IDisposable + { + if (_resources.TryGetValue(key, out var objResource)) + { + _resources.Remove(key); + if (objResource is T resource) + { + resource.Dispose(); + return resource; + } + } + + return default; + } + + /// + /// Adds a resource to this instance's collection with the specified key + /// + /// The type of the resource being added. + /// The resource that was added + internal T AddResource(TypedResourceKey key, T resource) => AddResource(key.Key, resource); + + /// + /// Retrieves a resource with the specified key and type if it exists + /// + /// The type of the resource being retrieved. + /// True if the resource exists, false otherwise + internal bool TryGetResource(TypedResourceKey key, out T resource) => TryGetResource(key.Key, out resource); + + /// + /// Retries a resource with the specified key and type + /// + /// The type of the resource being retrieved. + /// The resource if it exists or a default value. + internal T GetResource(TypedResourceKey key) => GetResource(key.Key); + + /// + /// Removes an existing resource with the specified key and type + /// + /// The type of the resource being removed. + /// The resource that was removed, if any + internal T RemoveResource(TypedResourceKey key) => RemoveResource(key.Key); + + /// + /// Removes an existing resource with the specified key and type, and disposes it + /// + /// The type of the resource being removed. + /// The resource that was removed, if any + internal T RemoveAndDisposeResource(TypedResourceKey key) + where T : IDisposable => RemoveAndDisposeResource(key.Key); + + /// + /// Disposes of any resources that implement and then clears all resources + /// + public void ClearAndDisposeResources() + { + if (_resources != null) + { + foreach (var kvp in _resources) + { + (kvp.Value as IDisposable)?.Dispose(); + } + + _resources.Clear(); + } + } + } +} \ No newline at end of file diff --git a/components/Effects/src/Shadows/Effects.cs b/components/Effects/src/Shadows/Effects.cs new file mode 100644 index 00000000..fa137d24 --- /dev/null +++ b/components/Effects/src/Shadows/Effects.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// Helper class for attaching shadows to s. + /// + public static class Effects + { + /// + /// Gets the shadow attached to a by getting the value of the property. + /// + /// The the is attached to. + /// The that is attached to the FrameworkElement. + public static AttachedShadowBase GetShadow(FrameworkElement obj) + { + return (AttachedShadowBase)obj.GetValue(ShadowProperty); + } + + /// + /// Attaches a shadow to an element by setting the property. + /// + /// The to attach the shadow to. + /// The that will be attached to the element + public static void SetShadow(FrameworkElement obj, AttachedShadowBase value) + { + obj.SetValue(ShadowProperty, value); + } + + /// + /// Attached for setting an to a . + /// + public static readonly DependencyProperty ShadowProperty = + DependencyProperty.RegisterAttached("Shadow", typeof(AttachedShadowBase), typeof(Effects), new PropertyMetadata(null, OnShadowChanged)); + + private static void OnShadowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is FrameworkElement element)) + { + return; + } + + if (e.OldValue is AttachedShadowBase oldShadow) + { + oldShadow.DisconnectElement(element); + } + + if (e.NewValue is AttachedShadowBase newShadow) + { + newShadow.ConnectElement(element); + } + } + } +} diff --git a/components/Effects/src/Shadows/IAlphaMaskProvider.cs b/components/Effects/src/Shadows/IAlphaMaskProvider.cs new file mode 100644 index 00000000..e1b794bf --- /dev/null +++ b/components/Effects/src/Shadows/IAlphaMaskProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Composition; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// Any user control can implement this interface to provide a custom alpha mask to it's parent DropShadowPanel + /// + public interface IAlphaMaskProvider + { + /// + /// Gets a value indicating whether the AlphaMask needs to be retrieved after the element has loaded. + /// + bool WaitUntilLoaded { get; } + + /// + /// This method should return the appropiate alpha mask to be used in the shadow of this control + /// + /// The alpha mask as a composition brush + CompositionBrush GetAlphaMask(); + } +} \ No newline at end of file diff --git a/components/Effects/src/Shadows/IAttachedShadow.cs b/components/Effects/src/Shadows/IAttachedShadow.cs new file mode 100644 index 00000000..a889bbe9 --- /dev/null +++ b/components/Effects/src/Shadows/IAttachedShadow.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation 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 System.Numerics; +using Microsoft.UI.Xaml; +using Windows.UI; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// Interface representing the common properties found within an attached shadow, for implementation. + /// + public interface IAttachedShadow + { + /// + /// Gets or sets the blur radius of the shadow. + /// + double BlurRadius { get; set; } + + /// + /// Gets or sets the opacity of the shadow. + /// + double Opacity { get; set; } + + /// + /// Gets or sets the offset of the shadow as a string representation of a . + /// + string Offset { get; set; } + + /// + /// Gets or sets the color of the shadow. + /// + Color Color { get; set; } + + /// + /// Get the associated for the specified . + /// + /// The for the element. + AttachedShadowElementContext GetElementContext(FrameworkElement element); + + /// + /// Gets an enumeration over the current list of of elements using this shared shadow definition. + /// + /// Enumeration of objects. + IEnumerable EnumerateElementContexts(); + } +} diff --git a/components/Effects/src/Shadows/TypedResourceKey.cs b/components/Effects/src/Shadows/TypedResourceKey.cs new file mode 100644 index 00000000..a398d62d --- /dev/null +++ b/components/Effects/src/Shadows/TypedResourceKey.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.WinUI.UI +{ + /// + /// A generic class that can be used to retrieve keyed resources of the specified type. + /// + /// The of resource the will retrieve. + internal sealed class TypedResourceKey + { + /// + /// Initializes a new instance of the class with the specified key. + /// + /// The resource's key + public TypedResourceKey(string key) => Key = key; + + /// + /// Gets the key of the resource to be retrieved. + /// + public string Key { get; } + + /// + /// Implicit operator for transforming a string into a key. + /// + /// The key string. + public static implicit operator TypedResourceKey(string key) + { + return new TypedResourceKey(key); + } + } +} diff --git a/components/Effects/tests/Effects.Tests.projitems b/components/Effects/tests/Effects.Tests.projitems new file mode 100644 index 00000000..75cbddd6 --- /dev/null +++ b/components/Effects/tests/Effects.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 1831A22E-6A21-48CA-B4E5-84ED04E6368F + + + EffectsExperiment.Tests + + + + + ExampleEffectsTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Effects/tests/Effects.Tests.shproj b/components/Effects/tests/Effects.Tests.shproj new file mode 100644 index 00000000..9c2211b6 --- /dev/null +++ b/components/Effects/tests/Effects.Tests.shproj @@ -0,0 +1,13 @@ + + + + 1831A22E-6A21-48CA-B4E5-84ED04E6368F + 14.0 + + + + + + + + diff --git a/components/Effects/tests/ExampleEffectsTestClass.cs b/components/Effects/tests/ExampleEffectsTestClass.cs new file mode 100644 index 00000000..f1cb7e72 --- /dev/null +++ b/components/Effects/tests/ExampleEffectsTestClass.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls; + +namespace EffectsExperiment.Tests; + +[TestClass] +public partial class ExampleEffectsTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Effects).Assembly; + var type = assembly.GetType(typeof(Effects).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Effects type."); + Assert.AreEqual(typeof(Effects), type, "Type of Effects does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new Effects(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleEffectsTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("EffectsControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleEffectsTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new Effects_ClassicBinding(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new Effects_ClassicBinding(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new Effects_ClassicBinding(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/Effects/tests/ExampleEffectsTestPage.xaml b/components/Effects/tests/ExampleEffectsTestPage.xaml new file mode 100644 index 00000000..8ae9f027 --- /dev/null +++ b/components/Effects/tests/ExampleEffectsTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Effects/tests/ExampleEffectsTestPage.xaml.cs b/components/Effects/tests/ExampleEffectsTestPage.xaml.cs new file mode 100644 index 00000000..80d995a2 --- /dev/null +++ b/components/Effects/tests/ExampleEffectsTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace EffectsExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleEffectsTestPage : Page +{ + public ExampleEffectsTestPage() + { + this.InitializeComponent(); + } +} From ff7280bd182b3496117902afca074662a9acbeb4 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 12 Apr 2023 23:18:20 -0700 Subject: [PATCH 2/7] Move to file-scoped namespaces (no other changes) --- components/Effects/samples/Effects.md | 7 - .../Effects/src/Shadows/AttachedDropShadow.cs | 537 +++++++++--------- .../Effects/src/Shadows/AttachedShadowBase.cs | 417 +++++++------- .../Shadows/AttachedShadowElementContext.cs | 491 ++++++++-------- components/Effects/src/Shadows/Effects.cs | 75 ++- .../Effects/src/Shadows/IAlphaMaskProvider.cs | 29 +- .../Effects/src/Shadows/IAttachedShadow.cs | 71 ++- .../Effects/src/Shadows/TypedResourceKey.cs | 43 +- 8 files changed, 828 insertions(+), 842 deletions(-) diff --git a/components/Effects/samples/Effects.md b/components/Effects/samples/Effects.md index 69a95b29..41c4e509 100644 --- a/components/Effects/samples/Effects.md +++ b/components/Effects/samples/Effects.md @@ -23,13 +23,6 @@ issue-id: 0 TODO: Fill in information about this experiment and how to get started here... -## Custom Control - -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: - -> [!Sample EffectsCustomSample] - ## Templated Controls The Toolkit is built with templated controls. This provides developers a flexible way to restyle components diff --git a/components/Effects/src/Shadows/AttachedDropShadow.cs b/components/Effects/src/Shadows/AttachedDropShadow.cs index b2512fdb..4240cdb8 100644 --- a/components/Effects/src/Shadows/AttachedDropShadow.cs +++ b/components/Effects/src/Shadows/AttachedDropShadow.cs @@ -13,359 +13,358 @@ using Microsoft.UI.Xaml.Shapes; using Windows.Foundation; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// A helper to add a composition based drop shadow to a . +/// +public sealed class AttachedDropShadow : AttachedShadowBase { + private const float MaxBlurRadius = 72; + + /// + protected internal override bool SupportsOnSizeChangedEvent => true; + + private static readonly TypedResourceKey RoundedRectangleGeometryResourceKey = "RoundedGeometry"; + private static readonly TypedResourceKey ShapeResourceKey = "Shape"; + private static readonly TypedResourceKey ShapeVisualResourceKey = "ShapeVisual"; + private static readonly TypedResourceKey SurfaceBrushResourceKey = "SurfaceBrush"; + private static readonly TypedResourceKey VisualSurfaceResourceKey = "VisualSurface"; + /// - /// A helper to add a composition based drop shadow to a . + /// Gets or sets a value indicating whether the panel uses an alpha mask to create a more precise shadow vs. a quicker rectangle shape. /// - public sealed class AttachedDropShadow : AttachedShadowBase + /// + /// Turn this off to lose fidelity and gain performance of the panel. + /// + public bool IsMasked { - private const float MaxBlurRadius = 72; - - /// - protected internal override bool SupportsOnSizeChangedEvent => true; - - private static readonly TypedResourceKey RoundedRectangleGeometryResourceKey = "RoundedGeometry"; - private static readonly TypedResourceKey ShapeResourceKey = "Shape"; - private static readonly TypedResourceKey ShapeVisualResourceKey = "ShapeVisual"; - private static readonly TypedResourceKey SurfaceBrushResourceKey = "SurfaceBrush"; - private static readonly TypedResourceKey VisualSurfaceResourceKey = "VisualSurface"; - - /// - /// Gets or sets a value indicating whether the panel uses an alpha mask to create a more precise shadow vs. a quicker rectangle shape. - /// - /// - /// Turn this off to lose fidelity and gain performance of the panel. - /// - public bool IsMasked - { - get { return (bool)GetValue(IsMaskedProperty); } - set { SetValue(IsMaskedProperty, value); } - } + get { return (bool)GetValue(IsMaskedProperty); } + set { SetValue(IsMaskedProperty, value); } + } - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty IsMaskedProperty = - DependencyProperty.Register(nameof(IsMasked), typeof(bool), typeof(AttachedDropShadow), new PropertyMetadata(true, OnDependencyPropertyChanged)); + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsMaskedProperty = + DependencyProperty.Register(nameof(IsMasked), typeof(bool), typeof(AttachedDropShadow), new PropertyMetadata(true, OnDependencyPropertyChanged)); - /// - /// Gets or sets the roundness of the shadow's corners. - /// - public double CornerRadius - { - get => (double)GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } + /// + /// Gets or sets the roundness of the shadow's corners. + /// + public double CornerRadius + { + get => (double)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - /// - /// The for - /// - public static readonly DependencyProperty CornerRadiusProperty = - DependencyProperty.Register( - nameof(CornerRadius), - typeof(double), - typeof(AttachedDropShadow), - new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4 - - /// - /// Gets or sets the to be used as a backdrop to cast shadows on. - /// - public FrameworkElement CastTo - { - get { return (FrameworkElement)GetValue(CastToProperty); } - set { SetValue(CastToProperty, value); } - } + /// + /// The for + /// + public static readonly DependencyProperty CornerRadiusProperty = + DependencyProperty.Register( + nameof(CornerRadius), + typeof(double), + typeof(AttachedDropShadow), + new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4 - /// - /// The for - /// - public static readonly DependencyProperty CastToProperty = - DependencyProperty.Register(nameof(CastTo), typeof(FrameworkElement), typeof(AttachedDropShadow), new PropertyMetadata(null, OnCastToPropertyChanged)); // TODO: Property Change + /// + /// Gets or sets the to be used as a backdrop to cast shadows on. + /// + public FrameworkElement CastTo + { + get { return (FrameworkElement)GetValue(CastToProperty); } + set { SetValue(CastToProperty, value); } + } - private ContainerVisual _container; + /// + /// The for + /// + public static readonly DependencyProperty CastToProperty = + DependencyProperty.Register(nameof(CastTo), typeof(FrameworkElement), typeof(AttachedDropShadow), new PropertyMetadata(null, OnCastToPropertyChanged)); // TODO: Property Change + + private ContainerVisual _container; - private static void OnCastToPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnCastToPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AttachedDropShadow shadow) { - if (d is AttachedDropShadow shadow) + if (e.OldValue is FrameworkElement element) + { + ElementCompositionPreview.SetElementChildVisual(element, null); + element.SizeChanged -= shadow.CastToElement_SizeChanged; + } + + if (e.NewValue is FrameworkElement elementNew) { - if (e.OldValue is FrameworkElement element) + var prevContainer = shadow._container; + + var child = ElementCompositionPreview.GetElementChildVisual(elementNew); + if (child is ContainerVisual visual) { - ElementCompositionPreview.SetElementChildVisual(element, null); - element.SizeChanged -= shadow.CastToElement_SizeChanged; + shadow._container = visual; } - - if (e.NewValue is FrameworkElement elementNew) + else { - var prevContainer = shadow._container; + var compositor = ElementCompositionPreview.GetElementVisual(shadow.CastTo).Compositor; + shadow._container = compositor.CreateContainerVisual(); - var child = ElementCompositionPreview.GetElementChildVisual(elementNew); - if (child is ContainerVisual visual) - { - shadow._container = visual; - } - else - { - var compositor = ElementCompositionPreview.GetElementVisual(shadow.CastTo).Compositor; - shadow._container = compositor.CreateContainerVisual(); - - ElementCompositionPreview.SetElementChildVisual(elementNew, shadow._container); - } + ElementCompositionPreview.SetElementChildVisual(elementNew, shadow._container); + } - // Need to remove all old children from previous container if it's changed - if (prevContainer != null && prevContainer != shadow._container) + // Need to remove all old children from previous container if it's changed + if (prevContainer != null && prevContainer != shadow._container) + { + foreach (var context in shadow.EnumerateElementContexts()) { - foreach (var context in shadow.EnumerateElementContexts()) + if (context.IsInitialized && + prevContainer.Children.Contains(context.SpriteVisual)) { - if (context.IsInitialized && - prevContainer.Children.Contains(context.SpriteVisual)) - { - prevContainer.Children.Remove(context.SpriteVisual); - } + prevContainer.Children.Remove(context.SpriteVisual); } } + } - // Make sure all child shadows are hooked into container - foreach (var context in shadow.EnumerateElementContexts()) + // Make sure all child shadows are hooked into container + foreach (var context in shadow.EnumerateElementContexts()) + { + if (context.IsInitialized) { - if (context.IsInitialized) - { - shadow.SetElementChildVisual(context); - } + shadow.SetElementChildVisual(context); } + } - elementNew.SizeChanged += shadow.CastToElement_SizeChanged; + elementNew.SizeChanged += shadow.CastToElement_SizeChanged; - // Re-trigger updates to all shadow locations for new parent - shadow.CastToElement_SizeChanged(null, null); - } + // Re-trigger updates to all shadow locations for new parent + shadow.CastToElement_SizeChanged(null, null); } } + } - private void CastToElement_SizeChanged(object sender, SizeChangedEventArgs e) + private void CastToElement_SizeChanged(object sender, SizeChangedEventArgs e) + { + // Don't use sender or 'e' here as related to container element not + // element for shadow, grab values off context. (Also may be null from internal call.) + foreach (var context in EnumerateElementContexts()) { - // Don't use sender or 'e' here as related to container element not - // element for shadow, grab values off context. (Also may be null from internal call.) - foreach (var context in EnumerateElementContexts()) + if (context.IsInitialized) { - if (context.IsInitialized) - { - // TODO: Should we use ActualWidth/Height instead of RenderSize? - OnSizeChanged(context, context.Element.RenderSize, context.Element.RenderSize); - } + // TODO: Should we use ActualWidth/Height instead of RenderSize? + OnSizeChanged(context, context.Element.RenderSize, context.Element.RenderSize); } } + } - /// - protected internal override void OnElementContextUninitialized(AttachedShadowElementContext context) + /// + protected internal override void OnElementContextUninitialized(AttachedShadowElementContext context) + { + if (_container != null && _container.Children.Contains(context.SpriteVisual)) { - if (_container != null && _container.Children.Contains(context.SpriteVisual)) - { - _container.Children.Remove(context.SpriteVisual); - } + _container.Children.Remove(context.SpriteVisual); + } - context.SpriteVisual?.StopAnimation("Size"); + context.SpriteVisual?.StopAnimation("Size"); - context.Element.LayoutUpdated -= Element_LayoutUpdated; + context.Element.LayoutUpdated -= Element_LayoutUpdated; - if (context.VisibilityToken != null) - { - context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); - context.VisibilityToken = null; - } - - base.OnElementContextUninitialized(context); - } - - /// - protected override void SetElementChildVisual(AttachedShadowElementContext context) + if (context.VisibilityToken != null) { - if (_container != null && !_container.Children.Contains(context.SpriteVisual)) - { - _container.Children.InsertAtTop(context.SpriteVisual); - } - - // Handles size changing and other elements around it updating. - context.Element.LayoutUpdated -= Element_LayoutUpdated; - context.Element.LayoutUpdated += Element_LayoutUpdated; + context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); + context.VisibilityToken = null; + } - if (context.VisibilityToken != null) - { - context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); - context.VisibilityToken = null; - } + base.OnElementContextUninitialized(context); + } - context.VisibilityToken = context.Element.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, Element_VisibilityChanged); + /// + protected override void SetElementChildVisual(AttachedShadowElementContext context) + { + if (_container != null && !_container.Children.Contains(context.SpriteVisual)) + { + _container.Children.InsertAtTop(context.SpriteVisual); } - private void Element_LayoutUpdated(object sender, object e) + // Handles size changing and other elements around it updating. + context.Element.LayoutUpdated -= Element_LayoutUpdated; + context.Element.LayoutUpdated += Element_LayoutUpdated; + + if (context.VisibilityToken != null) { - // Update other shadows to account for layout changes - CastToElement_SizeChanged(null, null); + context.Element.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, context.VisibilityToken.Value); + context.VisibilityToken = null; } - private void Element_VisibilityChanged(DependencyObject sender, DependencyProperty dp) + context.VisibilityToken = context.Element.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, Element_VisibilityChanged); + } + + private void Element_LayoutUpdated(object sender, object e) + { + // Update other shadows to account for layout changes + CastToElement_SizeChanged(null, null); + } + + private void Element_VisibilityChanged(DependencyObject sender, DependencyProperty dp) + { + if (sender is FrameworkElement element) { - if (sender is FrameworkElement element) - { - var context = GetElementContext(element); + var context = GetElementContext(element); - if (element.Visibility == Visibility.Collapsed) + if (element.Visibility == Visibility.Collapsed) + { + if (_container != null && _container.Children.Contains(context.SpriteVisual)) { - if (_container != null && _container.Children.Contains(context.SpriteVisual)) - { - _container.Children.Remove(context.SpriteVisual); - } + _container.Children.Remove(context.SpriteVisual); } - else + } + else + { + if (_container != null && !_container.Children.Contains(context.SpriteVisual)) { - if (_container != null && !_container.Children.Contains(context.SpriteVisual)) - { - _container.Children.InsertAtTop(context.SpriteVisual); - } + _container.Children.InsertAtTop(context.SpriteVisual); } } - - // Update other shadows to account for layout changes - CastToElement_SizeChanged(null, null); } - /// - protected override CompositionBrush GetShadowMask(AttachedShadowElementContext context) - { - CompositionBrush mask = null; + // Update other shadows to account for layout changes + CastToElement_SizeChanged(null, null); + } - if (DesignTimeHelpers.IsRunningInLegacyDesignerMode) - { - return null; - } + /// + protected override CompositionBrush GetShadowMask(AttachedShadowElementContext context) + { + CompositionBrush mask = null; - if (context.Element != null) + if (DesignTimeHelpers.IsRunningInLegacyDesignerMode) + { + return null; + } + + if (context.Element != null) + { + if (IsMasked) { - if (IsMasked) + // We check for IAlphaMaskProvider first, to ensure that we use the custom + // alpha mask even if Content happens to extend any of the other classes + if (context.Element is IAlphaMaskProvider maskedControl) { - // We check for IAlphaMaskProvider first, to ensure that we use the custom - // alpha mask even if Content happens to extend any of the other classes - if (context.Element is IAlphaMaskProvider maskedControl) - { - if (maskedControl.WaitUntilLoaded && !context.Element.IsLoaded) - { - context.Element.Loaded += CustomMaskedElement_Loaded; - } - else - { - mask = maskedControl.GetAlphaMask(); - } - } - else if (context.Element is Image) + if (maskedControl.WaitUntilLoaded && !context.Element.IsLoaded) { - mask = ((Image)context.Element).GetAlphaMask(); + context.Element.Loaded += CustomMaskedElement_Loaded; } - else if (context.Element is Shape) - { - mask = ((Shape)context.Element).GetAlphaMask(); - } - else if (context.Element is TextBlock) + else { - mask = ((TextBlock)context.Element).GetAlphaMask(); + mask = maskedControl.GetAlphaMask(); } } - - // If we don't have a mask and have specified rounded corners, we'll generate a simple quick mask. - // This is the same code from link:AttachedCardShadow.cs:GetShadowMask - if (mask == null && CornerRadius > 0) + else if (context.Element is Image) { - // Create rounded rectangle geometry and add it to a shape - var geometry = context.GetResource(RoundedRectangleGeometryResourceKey) ?? context.AddResource( - RoundedRectangleGeometryResourceKey, - context.Compositor.CreateRoundedRectangleGeometry()); - geometry.CornerRadius = new Vector2((float)CornerRadius); - - var shape = context.GetResource(ShapeResourceKey) ?? context.AddResource(ShapeResourceKey, context.Compositor.CreateSpriteShape(geometry)); - shape.FillBrush = context.Compositor.CreateColorBrush(Colors.Black); - - // Create a ShapeVisual so that our geometry can be rendered to a visual - var shapeVisual = context.GetResource(ShapeVisualResourceKey) ?? - context.AddResource(ShapeVisualResourceKey, context.Compositor.CreateShapeVisual()); - shapeVisual.Shapes.Add(shape); - - // Create a CompositionVisualSurface, which renders our ShapeVisual to a texture - var visualSurface = context.GetResource(VisualSurfaceResourceKey) ?? - context.AddResource(VisualSurfaceResourceKey, context.Compositor.CreateVisualSurface()); - visualSurface.SourceVisual = shapeVisual; - - // Create a CompositionSurfaceBrush to render our CompositionVisualSurface to a brush. - // Now we have a rounded rectangle brush that can be used on as the mask for our shadow. - var surfaceBrush = context.GetResource(SurfaceBrushResourceKey) ?? context.AddResource( - SurfaceBrushResourceKey, - context.Compositor.CreateSurfaceBrush(visualSurface)); - - geometry.Size = visualSurface.SourceSize = shapeVisual.Size = context.Element.RenderSize.ToVector2(); - - mask = surfaceBrush; + mask = ((Image)context.Element).GetAlphaMask(); + } + else if (context.Element is Shape) + { + mask = ((Shape)context.Element).GetAlphaMask(); + } + else if (context.Element is TextBlock) + { + mask = ((TextBlock)context.Element).GetAlphaMask(); } } - // Position our shadow in the correct spot to match the corresponding element. - context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); + // If we don't have a mask and have specified rounded corners, we'll generate a simple quick mask. + // This is the same code from link:AttachedCardShadow.cs:GetShadowMask + if (mask == null && CornerRadius > 0) + { + // Create rounded rectangle geometry and add it to a shape + var geometry = context.GetResource(RoundedRectangleGeometryResourceKey) ?? context.AddResource( + RoundedRectangleGeometryResourceKey, + context.Compositor.CreateRoundedRectangleGeometry()); + geometry.CornerRadius = new Vector2((float)CornerRadius); + + var shape = context.GetResource(ShapeResourceKey) ?? context.AddResource(ShapeResourceKey, context.Compositor.CreateSpriteShape(geometry)); + shape.FillBrush = context.Compositor.CreateColorBrush(Colors.Black); + + // Create a ShapeVisual so that our geometry can be rendered to a visual + var shapeVisual = context.GetResource(ShapeVisualResourceKey) ?? + context.AddResource(ShapeVisualResourceKey, context.Compositor.CreateShapeVisual()); + shapeVisual.Shapes.Add(shape); + + // Create a CompositionVisualSurface, which renders our ShapeVisual to a texture + var visualSurface = context.GetResource(VisualSurfaceResourceKey) ?? + context.AddResource(VisualSurfaceResourceKey, context.Compositor.CreateVisualSurface()); + visualSurface.SourceVisual = shapeVisual; + + // Create a CompositionSurfaceBrush to render our CompositionVisualSurface to a brush. + // Now we have a rounded rectangle brush that can be used on as the mask for our shadow. + var surfaceBrush = context.GetResource(SurfaceBrushResourceKey) ?? context.AddResource( + SurfaceBrushResourceKey, + context.Compositor.CreateSurfaceBrush(visualSurface)); + + geometry.Size = visualSurface.SourceSize = shapeVisual.Size = context.Element.RenderSize.ToVector2(); + + mask = surfaceBrush; + } + } - BindSizeAndScale(context.SpriteVisual, context.Element); + // Position our shadow in the correct spot to match the corresponding element. + context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); - return mask; - } + BindSizeAndScale(context.SpriteVisual, context.Element); - private static void BindSizeAndScale(CompositionObject source, UIElement target) - { - var visual = ElementCompositionPreview.GetElementVisual(target); - var bindSizeAnimation = source.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size * {nameof(visual)}.Scale.XY"); + return mask; + } - bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); + private static void BindSizeAndScale(CompositionObject source, UIElement target) + { + var visual = ElementCompositionPreview.GetElementVisual(target); + var bindSizeAnimation = source.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size * {nameof(visual)}.Scale.XY"); - // Start the animation - source.StartAnimation("Size", bindSizeAnimation); - } + bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); - private void CustomMaskedElement_Loaded(object sender, RoutedEventArgs e) - { - var context = GetElementContext(sender as FrameworkElement); + // Start the animation + source.StartAnimation("Size", bindSizeAnimation); + } - context.Element.Loaded -= CustomMaskedElement_Loaded; + private void CustomMaskedElement_Loaded(object sender, RoutedEventArgs e) + { + var context = GetElementContext(sender as FrameworkElement); - UpdateShadowClip(context); - UpdateShadowMask(context); - } + context.Element.Loaded -= CustomMaskedElement_Loaded; - /// - protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) - { - context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); + UpdateShadowClip(context); + UpdateShadowMask(context); + } - UpdateShadowClip(context); + /// + protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) + { + context.SpriteVisual.Offset = context.Element.CoordinatesFrom(CastTo).ToVector3(); - base.OnSizeChanged(context, newSize, previousSize); - } + UpdateShadowClip(context); - /// - protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + base.OnSizeChanged(context, newSize, previousSize); + } + + /// + protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + { + if (property == IsMaskedProperty) { - if (property == IsMaskedProperty) + UpdateShadowMask(context); + } + else if (property == CornerRadiusProperty) + { + var geometry = context.GetResource(RoundedRectangleGeometryResourceKey); + if (geometry != null) { - UpdateShadowMask(context); + geometry.CornerRadius = new Vector2((float)(double)newValue); } - else if (property == CornerRadiusProperty) - { - var geometry = context.GetResource(RoundedRectangleGeometryResourceKey); - if (geometry != null) - { - geometry.CornerRadius = new Vector2((float)(double)newValue); - } - UpdateShadowMask(context); - } - else - { - base.OnPropertyChanged(context, property, oldValue, newValue); - } + UpdateShadowMask(context); + } + else + { + base.OnPropertyChanged(context, property, oldValue, newValue); } } } diff --git a/components/Effects/src/Shadows/AttachedShadowBase.cs b/components/Effects/src/Shadows/AttachedShadowBase.cs index 9794f0b8..606fee07 100644 --- a/components/Effects/src/Shadows/AttachedShadowBase.cs +++ b/components/Effects/src/Shadows/AttachedShadowBase.cs @@ -13,264 +13,263 @@ using Windows.Foundation.Metadata; using Windows.UI; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// The base class for attached shadows. +/// +public abstract class AttachedShadowBase : DependencyObject, IAttachedShadow { /// - /// The base class for attached shadows. + /// The for . + /// + public static readonly DependencyProperty BlurRadiusProperty = + DependencyProperty.Register(nameof(BlurRadius), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(12d, OnDependencyPropertyChanged)); + + /// + /// The for . + /// + public static readonly DependencyProperty ColorProperty = + DependencyProperty.Register(nameof(Color), typeof(Color), typeof(AttachedShadowBase), new PropertyMetadata(Colors.Black, OnDependencyPropertyChanged)); + + /// + /// The for . + /// + public static readonly DependencyProperty OffsetProperty = + DependencyProperty.Register( + nameof(Offset), + typeof(string), // Needs to be string as we can't convert in XAML natively from Vector3, see https://github.com/microsoft/microsoft-ui-xaml/issues/3896 + typeof(AttachedShadowBase), + new PropertyMetadata(string.Empty, OnDependencyPropertyChanged)); + + /// + /// The for + /// + public static readonly DependencyProperty OpacityProperty = + DependencyProperty.Register(nameof(Opacity), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(1d, OnDependencyPropertyChanged)); + + /// + /// Gets or sets the collection of for each element this is connected to. /// - public abstract class AttachedShadowBase : DependencyObject, IAttachedShadow + private ConditionalWeakTable ShadowElementContextTable { get; set; } + + /// + public double BlurRadius { - /// - /// The for . - /// - public static readonly DependencyProperty BlurRadiusProperty = - DependencyProperty.Register(nameof(BlurRadius), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(12d, OnDependencyPropertyChanged)); - - /// - /// The for . - /// - public static readonly DependencyProperty ColorProperty = - DependencyProperty.Register(nameof(Color), typeof(Color), typeof(AttachedShadowBase), new PropertyMetadata(Colors.Black, OnDependencyPropertyChanged)); - - /// - /// The for . - /// - public static readonly DependencyProperty OffsetProperty = - DependencyProperty.Register( - nameof(Offset), - typeof(string), // Needs to be string as we can't convert in XAML natively from Vector3, see https://github.com/microsoft/microsoft-ui-xaml/issues/3896 - typeof(AttachedShadowBase), - new PropertyMetadata(string.Empty, OnDependencyPropertyChanged)); - - /// - /// The for - /// - public static readonly DependencyProperty OpacityProperty = - DependencyProperty.Register(nameof(Opacity), typeof(double), typeof(AttachedShadowBase), new PropertyMetadata(1d, OnDependencyPropertyChanged)); - - /// - /// Gets or sets the collection of for each element this is connected to. - /// - private ConditionalWeakTable ShadowElementContextTable { get; set; } - - /// - public double BlurRadius - { - get => (double)GetValue(BlurRadiusProperty); - set => SetValue(BlurRadiusProperty, value); - } + get => (double)GetValue(BlurRadiusProperty); + set => SetValue(BlurRadiusProperty, value); + } - /// - public double Opacity - { - get => (double)GetValue(OpacityProperty); - set => SetValue(OpacityProperty, value); - } + /// + public double Opacity + { + get => (double)GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } - /// - public string Offset - { - get => (string)GetValue(OffsetProperty); - set => SetValue(OffsetProperty, value); - } + /// + public string Offset + { + get => (string)GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } + + /// + public Color Color + { + get => (Color)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } - /// - public Color Color + /// + /// Gets a value indicating whether or not OnSizeChanged should be called when is fired. + /// + protected internal abstract bool SupportsOnSizeChangedEvent { get; } + + /// + /// Use this method as the for DependencyProperties in derived classes. + /// + protected static void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + (sender as AttachedShadowBase)?.CallPropertyChangedForEachElement(args.Property, args.OldValue, args.NewValue); + } + + internal void ConnectElement(FrameworkElement element) + { + ShadowElementContextTable = ShadowElementContextTable ?? new ConditionalWeakTable(); + if (ShadowElementContextTable.TryGetValue(element, out var context)) { - get => (Color)GetValue(ColorProperty); - set => SetValue(ColorProperty, value); + return; } - /// - /// Gets a value indicating whether or not OnSizeChanged should be called when is fired. - /// - protected internal abstract bool SupportsOnSizeChangedEvent { get; } + context = new AttachedShadowElementContext(); + context.ConnectToElement(this, element); + ShadowElementContextTable.Add(element, context); + } - /// - /// Use this method as the for DependencyProperties in derived classes. - /// - protected static void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + internal void DisconnectElement(FrameworkElement element) + { + if (ShadowElementContextTable == null) { - (sender as AttachedShadowBase)?.CallPropertyChangedForEachElement(args.Property, args.OldValue, args.NewValue); + return; } - internal void ConnectElement(FrameworkElement element) + if (ShadowElementContextTable.TryGetValue(element, out var context)) { - ShadowElementContextTable = ShadowElementContextTable ?? new ConditionalWeakTable(); - if (ShadowElementContextTable.TryGetValue(element, out var context)) - { - return; - } - - context = new AttachedShadowElementContext(); - context.ConnectToElement(this, element); - ShadowElementContextTable.Add(element, context); + context.DisconnectFromElement(); + ShadowElementContextTable.Remove(element); } + } - internal void DisconnectElement(FrameworkElement element) - { - if (ShadowElementContextTable == null) - { - return; - } + /// + /// Override to handle when the for an element is being initialized. + /// + /// The that is being initialized. + protected internal virtual void OnElementContextInitialized(AttachedShadowElementContext context) + { + OnPropertyChanged(context, OpacityProperty, Opacity, Opacity); + OnPropertyChanged(context, BlurRadiusProperty, BlurRadius, BlurRadius); + OnPropertyChanged(context, ColorProperty, Color, Color); + OnPropertyChanged(context, OffsetProperty, Offset, Offset); + UpdateShadowClip(context); + UpdateShadowMask(context); + SetElementChildVisual(context); + } - if (ShadowElementContextTable.TryGetValue(element, out var context)) - { - context.DisconnectFromElement(); - ShadowElementContextTable.Remove(element); - } - } + /// + /// Override to handle when the for an element is being uninitialized. + /// + /// The that is being uninitialized. + protected internal virtual void OnElementContextUninitialized(AttachedShadowElementContext context) + { + context.ClearAndDisposeResources(); + ElementCompositionPreview.SetElementChildVisual(context.Element, null); + } - /// - /// Override to handle when the for an element is being initialized. - /// - /// The that is being initialized. - protected internal virtual void OnElementContextInitialized(AttachedShadowElementContext context) + /// + public AttachedShadowElementContext GetElementContext(FrameworkElement element) + { + if (ShadowElementContextTable != null && ShadowElementContextTable.TryGetValue(element, out var context)) { - OnPropertyChanged(context, OpacityProperty, Opacity, Opacity); - OnPropertyChanged(context, BlurRadiusProperty, BlurRadius, BlurRadius); - OnPropertyChanged(context, ColorProperty, Color, Color); - OnPropertyChanged(context, OffsetProperty, Offset, Offset); - UpdateShadowClip(context); - UpdateShadowMask(context); - SetElementChildVisual(context); + return context; } - /// - /// Override to handle when the for an element is being uninitialized. - /// - /// The that is being uninitialized. - protected internal virtual void OnElementContextUninitialized(AttachedShadowElementContext context) + return null; + } + + /// + public IEnumerable EnumerateElementContexts() + { + foreach (var kvp in ShadowElementContextTable) { - context.ClearAndDisposeResources(); - ElementCompositionPreview.SetElementChildVisual(context.Element, null); + yield return kvp.Value; } + } - /// - public AttachedShadowElementContext GetElementContext(FrameworkElement element) - { - if (ShadowElementContextTable != null && ShadowElementContextTable.TryGetValue(element, out var context)) - { - return context; - } + /// + /// Sets as a child visual on + /// + /// The this operaiton will be performed on. + protected virtual void SetElementChildVisual(AttachedShadowElementContext context) + { + ElementCompositionPreview.SetElementChildVisual(context.Element, context.SpriteVisual); + } - return null; + private void CallPropertyChangedForEachElement(DependencyProperty property, object oldValue, object newValue) + { + if (ShadowElementContextTable == null) + { + return; } - /// - public IEnumerable EnumerateElementContexts() + foreach (var context in ShadowElementContextTable) { - foreach (var kvp in ShadowElementContextTable) + if (context.Value.IsInitialized) { - yield return kvp.Value; + OnPropertyChanged(context.Value, property, oldValue, newValue); } } + } - /// - /// Sets as a child visual on - /// - /// The this operaiton will be performed on. - protected virtual void SetElementChildVisual(AttachedShadowElementContext context) - { - ElementCompositionPreview.SetElementChildVisual(context.Element, context.SpriteVisual); - } + /// + /// Get a in the shape of the element that is casting the shadow. + /// + /// A representing the shape of an element. + protected virtual CompositionBrush GetShadowMask(AttachedShadowElementContext context) + { + return null; + } - private void CallPropertyChangedForEachElement(DependencyProperty property, object oldValue, object newValue) - { - if (ShadowElementContextTable == null) - { - return; - } + /// + /// Get the for the shadow's + /// + /// A for the extent of the shadowed area. + protected virtual CompositionClip GetShadowClip(AttachedShadowElementContext context) + { + return null; + } - foreach (var context in ShadowElementContextTable) - { - if (context.Value.IsInitialized) - { - OnPropertyChanged(context.Value, property, oldValue, newValue); - } - } + /// + /// Update the mask that gives the shadow its shape. + /// + protected void UpdateShadowMask(AttachedShadowElementContext context) + { + if (!context.IsInitialized) + { + return; } - /// - /// Get a in the shape of the element that is casting the shadow. - /// - /// A representing the shape of an element. - protected virtual CompositionBrush GetShadowMask(AttachedShadowElementContext context) + context.Shadow.Mask = GetShadowMask(context); + } + + /// + /// Update the clipping on the shadow's . + /// + protected void UpdateShadowClip(AttachedShadowElementContext context) + { + if (!context.IsInitialized) { - return null; + return; } - /// - /// Get the for the shadow's - /// - /// A for the extent of the shadowed area. - protected virtual CompositionClip GetShadowClip(AttachedShadowElementContext context) + context.SpriteVisual.Clip = GetShadowClip(context); + } + + /// + /// This method is called when a DependencyProperty is changed. + /// + protected virtual void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + { + if (!context.IsInitialized) { - return null; + return; } - /// - /// Update the mask that gives the shadow its shape. - /// - protected void UpdateShadowMask(AttachedShadowElementContext context) + if (property == BlurRadiusProperty) { - if (!context.IsInitialized) - { - return; - } - - context.Shadow.Mask = GetShadowMask(context); + context.Shadow.BlurRadius = (float)(double)newValue; } - - /// - /// Update the clipping on the shadow's . - /// - protected void UpdateShadowClip(AttachedShadowElementContext context) + else if (property == OpacityProperty) { - if (!context.IsInitialized) - { - return; - } - - context.SpriteVisual.Clip = GetShadowClip(context); + context.Shadow.Opacity = (float)(double)newValue; } - - /// - /// This method is called when a DependencyProperty is changed. - /// - protected virtual void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) + else if (property == ColorProperty) { - if (!context.IsInitialized) - { - return; - } - - if (property == BlurRadiusProperty) - { - context.Shadow.BlurRadius = (float)(double)newValue; - } - else if (property == OpacityProperty) - { - context.Shadow.Opacity = (float)(double)newValue; - } - else if (property == ColorProperty) - { - context.Shadow.Color = (Color)newValue; - } - else if (property == OffsetProperty) - { - context.Shadow.Offset = (Vector3)(newValue as string)?.ToVector3(); - } + context.Shadow.Color = (Color)newValue; } - - /// - /// This method is called when the element size changes, and = true. - /// - /// The for the firing its SizeChanged event - /// The new size of the - /// The previous size of the - protected internal virtual void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) + else if (property == OffsetProperty) { + context.Shadow.Offset = (Vector3)(newValue as string)?.ToVector3(); } } -} \ No newline at end of file + + /// + /// This method is called when the element size changes, and = true. + /// + /// The for the firing its SizeChanged event + /// The new size of the + /// The previous size of the + protected internal virtual void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) + { + } +} diff --git a/components/Effects/src/Shadows/AttachedShadowElementContext.cs b/components/Effects/src/Shadows/AttachedShadowElementContext.cs index 75cf79d0..18e4e374 100644 --- a/components/Effects/src/Shadows/AttachedShadowElementContext.cs +++ b/components/Effects/src/Shadows/AttachedShadowElementContext.cs @@ -9,312 +9,311 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Hosting; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// Class which maintains the context of a for a particular linked to the definition of that shadow provided by the implementation being used. +/// +public sealed class AttachedShadowElementContext { + private bool _isConnected; + + private Dictionary _resources; + + internal long? VisibilityToken { get; set; } + + /// + /// Gets a value indicating whether or not this has been initialized. + /// + public bool IsInitialized { get; private set; } + + /// + /// Gets the that contains this . + /// + public AttachedShadowBase Parent { get; private set; } + + /// + /// Gets the this instance is attached to + /// + public FrameworkElement Element { get; private set; } + + /// + /// Gets the for the this instance is attached to. + /// + public Visual ElementVisual { get; private set; } + /// - /// Class which maintains the context of a for a particular linked to the definition of that shadow provided by the implementation being used. + /// Gets the for this instance. /// - public sealed class AttachedShadowElementContext + public Compositor Compositor { get; private set; } + + /// + /// Gets the that contains the shadow for this instance + /// + public SpriteVisual SpriteVisual { get; private set; } + + /// + /// Gets the that is rendered on this instance's + /// + public DropShadow Shadow { get; private set; } + + /// + /// Connects a to its parent definition. + /// + /// The that is using this context. + /// The that a shadow is being attached to. + internal void ConnectToElement(AttachedShadowBase parent, FrameworkElement element) { - private bool _isConnected; - - private Dictionary _resources; - - internal long? VisibilityToken { get; set; } - - /// - /// Gets a value indicating whether or not this has been initialized. - /// - public bool IsInitialized { get; private set; } - - /// - /// Gets the that contains this . - /// - public AttachedShadowBase Parent { get; private set; } - - /// - /// Gets the this instance is attached to - /// - public FrameworkElement Element { get; private set; } - - /// - /// Gets the for the this instance is attached to. - /// - public Visual ElementVisual { get; private set; } - - /// - /// Gets the for this instance. - /// - public Compositor Compositor { get; private set; } - - /// - /// Gets the that contains the shadow for this instance - /// - public SpriteVisual SpriteVisual { get; private set; } - - /// - /// Gets the that is rendered on this instance's - /// - public DropShadow Shadow { get; private set; } - - /// - /// Connects a to its parent definition. - /// - /// The that is using this context. - /// The that a shadow is being attached to. - internal void ConnectToElement(AttachedShadowBase parent, FrameworkElement element) + if (_isConnected) { - if (_isConnected) - { - throw new InvalidOperationException("This AttachedShadowElementContext has already been connected to an element"); - } - - _isConnected = true; - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); - Element = element ?? throw new ArgumentNullException(nameof(element)); - Element.Loaded += OnElementLoaded; - Element.Unloaded += OnElementUnloaded; - Initialize(); + throw new InvalidOperationException("This AttachedShadowElementContext has already been connected to an element"); } - internal void DisconnectFromElement() + _isConnected = true; + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + Element = element ?? throw new ArgumentNullException(nameof(element)); + Element.Loaded += OnElementLoaded; + Element.Unloaded += OnElementUnloaded; + Initialize(); + } + + internal void DisconnectFromElement() + { + if (!_isConnected) { - if (!_isConnected) - { - return; - } + return; + } - Uninitialize(); + Uninitialize(); - Element.Loaded -= OnElementLoaded; - Element.Unloaded -= OnElementUnloaded; - Element = null; + Element.Loaded -= OnElementLoaded; + Element.Unloaded -= OnElementUnloaded; + Element = null; - Parent = null; + Parent = null; - _isConnected = false; - } + _isConnected = false; + } - /// - /// Force early creation of this instance's resources, otherwise they will be created automatically when is loaded. - /// - public void CreateResources() => Initialize(true); + /// + /// Force early creation of this instance's resources, otherwise they will be created automatically when is loaded. + /// + public void CreateResources() => Initialize(true); - private void Initialize(bool forceIfNotLoaded = false) + private void Initialize(bool forceIfNotLoaded = false) + { + if (IsInitialized || !_isConnected || (!Element.IsLoaded && !forceIfNotLoaded)) { - if (IsInitialized || !_isConnected || (!Element.IsLoaded && !forceIfNotLoaded)) - { - return; - } + return; + } - IsInitialized = true; + IsInitialized = true; - ElementVisual = ElementCompositionPreview.GetElementVisual(Element); - Compositor = ElementVisual.Compositor; + ElementVisual = ElementCompositionPreview.GetElementVisual(Element); + Compositor = ElementVisual.Compositor; - Shadow = Compositor.CreateDropShadow(); + Shadow = Compositor.CreateDropShadow(); - SpriteVisual = Compositor.CreateSpriteVisual(); - SpriteVisual.RelativeSizeAdjustment = Vector2.One; - SpriteVisual.Shadow = Shadow; + SpriteVisual = Compositor.CreateSpriteVisual(); + SpriteVisual.RelativeSizeAdjustment = Vector2.One; + SpriteVisual.Shadow = Shadow; - if (Parent.SupportsOnSizeChangedEvent) - { - Element.SizeChanged += OnElementSizeChanged; - } - - Parent?.OnElementContextInitialized(this); + if (Parent.SupportsOnSizeChangedEvent) + { + Element.SizeChanged += OnElementSizeChanged; } - private void Uninitialize() + Parent?.OnElementContextInitialized(this); + } + + private void Uninitialize() + { + if (!IsInitialized) { - if (!IsInitialized) - { - return; - } + return; + } - IsInitialized = false; + IsInitialized = false; - Parent.OnElementContextUninitialized(this); + Parent.OnElementContextUninitialized(this); - SpriteVisual.Shadow = null; - SpriteVisual.Dispose(); + SpriteVisual.Shadow = null; + SpriteVisual.Dispose(); - Shadow.Dispose(); + Shadow.Dispose(); - ElementCompositionPreview.SetElementChildVisual(Element, null); + ElementCompositionPreview.SetElementChildVisual(Element, null); - Element.SizeChanged -= OnElementSizeChanged; + Element.SizeChanged -= OnElementSizeChanged; - SpriteVisual = null; - Shadow = null; - ElementVisual = null; - } + SpriteVisual = null; + Shadow = null; + ElementVisual = null; + } - private void OnElementUnloaded(object sender, RoutedEventArgs e) + private void OnElementUnloaded(object sender, RoutedEventArgs e) + { + Uninitialize(); + } + + private void OnElementLoaded(object sender, RoutedEventArgs e) + { + Initialize(); + } + + private void OnElementSizeChanged(object sender, SizeChangedEventArgs e) + { + Parent?.OnSizeChanged(this, e.NewSize, e.PreviousSize); + } + + /// + /// Adds a resource to this instance's resource dictionary with the specified key + /// + /// The type of the resource being added. + /// Key to use to lookup the resource later. + /// Object to store within the resource dictionary. + /// The added resource + public T AddResource(string key, T resource) + { + _resources = _resources ?? new Dictionary(); + if (_resources.ContainsKey(key)) { - Uninitialize(); + _resources[key] = resource; } - - private void OnElementLoaded(object sender, RoutedEventArgs e) + else { - Initialize(); + _resources.Add(key, resource); } - private void OnElementSizeChanged(object sender, SizeChangedEventArgs e) + return resource; + } + + /// + /// Retrieves a resource with the specified key and type if it exists + /// + /// The type of the resource being retrieved. + /// Key to use to lookup the resource. + /// Object to retrieved from the resource dictionary or default value. + /// True if the resource exists, false otherwise + public bool TryGetResource(string key, out T resource) + { + if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource) { - Parent?.OnSizeChanged(this, e.NewSize, e.PreviousSize); + resource = tResource; + return true; } - /// - /// Adds a resource to this instance's resource dictionary with the specified key - /// - /// The type of the resource being added. - /// Key to use to lookup the resource later. - /// Object to store within the resource dictionary. - /// The added resource - public T AddResource(string key, T resource) - { - _resources = _resources ?? new Dictionary(); - if (_resources.ContainsKey(key)) - { - _resources[key] = resource; - } - else - { - _resources.Add(key, resource); - } + resource = default; + return false; + } + /// + /// Retries a resource with the specified key and type + /// + /// The type of the resource being retrieved. + /// Key to use to lookup the resource. + /// The resource if available, otherwise default value. + public T GetResource(string key) + { + if (TryGetResource(key, out T resource)) + { return resource; } - /// - /// Retrieves a resource with the specified key and type if it exists - /// - /// The type of the resource being retrieved. - /// Key to use to lookup the resource. - /// Object to retrieved from the resource dictionary or default value. - /// True if the resource exists, false otherwise - public bool TryGetResource(string key, out T resource) + return default; + } + + /// + /// Removes an existing resource with the specified key and type + /// + /// The type of the resource being removed. + /// Key to use to lookup the resource. + /// The resource that was removed, if any + public T RemoveResource(string key) + { + if (_resources.TryGetValue(key, out var objResource)) { - if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource) + _resources.Remove(key); + if (objResource is T resource) { - resource = tResource; - return true; + return resource; } - - resource = default; - return false; } - /// - /// Retries a resource with the specified key and type - /// - /// The type of the resource being retrieved. - /// Key to use to lookup the resource. - /// The resource if available, otherwise default value. - public T GetResource(string key) + return default; + } + + /// + /// Removes an existing resource with the specified key and type, and disposes it + /// + /// The type of the resource being removed. + /// Key to use to lookup the resource. + /// The resource that was removed, if any + public T RemoveAndDisposeResource(string key) + where T : IDisposable + { + if (_resources.TryGetValue(key, out var objResource)) { - if (TryGetResource(key, out T resource)) + _resources.Remove(key); + if (objResource is T resource) { + resource.Dispose(); return resource; } - - return default; } - /// - /// Removes an existing resource with the specified key and type - /// - /// The type of the resource being removed. - /// Key to use to lookup the resource. - /// The resource that was removed, if any - public T RemoveResource(string key) - { - if (_resources.TryGetValue(key, out var objResource)) - { - _resources.Remove(key); - if (objResource is T resource) - { - return resource; - } - } + return default; + } - return default; - } + /// + /// Adds a resource to this instance's collection with the specified key + /// + /// The type of the resource being added. + /// The resource that was added + internal T AddResource(TypedResourceKey key, T resource) => AddResource(key.Key, resource); - /// - /// Removes an existing resource with the specified key and type, and disposes it - /// - /// The type of the resource being removed. - /// Key to use to lookup the resource. - /// The resource that was removed, if any - public T RemoveAndDisposeResource(string key) - where T : IDisposable - { - if (_resources.TryGetValue(key, out var objResource)) - { - _resources.Remove(key); - if (objResource is T resource) - { - resource.Dispose(); - return resource; - } - } + /// + /// Retrieves a resource with the specified key and type if it exists + /// + /// The type of the resource being retrieved. + /// True if the resource exists, false otherwise + internal bool TryGetResource(TypedResourceKey key, out T resource) => TryGetResource(key.Key, out resource); - return default; - } + /// + /// Retries a resource with the specified key and type + /// + /// The type of the resource being retrieved. + /// The resource if it exists or a default value. + internal T GetResource(TypedResourceKey key) => GetResource(key.Key); - /// - /// Adds a resource to this instance's collection with the specified key - /// - /// The type of the resource being added. - /// The resource that was added - internal T AddResource(TypedResourceKey key, T resource) => AddResource(key.Key, resource); - - /// - /// Retrieves a resource with the specified key and type if it exists - /// - /// The type of the resource being retrieved. - /// True if the resource exists, false otherwise - internal bool TryGetResource(TypedResourceKey key, out T resource) => TryGetResource(key.Key, out resource); - - /// - /// Retries a resource with the specified key and type - /// - /// The type of the resource being retrieved. - /// The resource if it exists or a default value. - internal T GetResource(TypedResourceKey key) => GetResource(key.Key); - - /// - /// Removes an existing resource with the specified key and type - /// - /// The type of the resource being removed. - /// The resource that was removed, if any - internal T RemoveResource(TypedResourceKey key) => RemoveResource(key.Key); - - /// - /// Removes an existing resource with the specified key and type, and disposes it - /// - /// The type of the resource being removed. - /// The resource that was removed, if any - internal T RemoveAndDisposeResource(TypedResourceKey key) - where T : IDisposable => RemoveAndDisposeResource(key.Key); - - /// - /// Disposes of any resources that implement and then clears all resources - /// - public void ClearAndDisposeResources() + /// + /// Removes an existing resource with the specified key and type + /// + /// The type of the resource being removed. + /// The resource that was removed, if any + internal T RemoveResource(TypedResourceKey key) => RemoveResource(key.Key); + + /// + /// Removes an existing resource with the specified key and type, and disposes it + /// + /// The type of the resource being removed. + /// The resource that was removed, if any + internal T RemoveAndDisposeResource(TypedResourceKey key) + where T : IDisposable => RemoveAndDisposeResource(key.Key); + + /// + /// Disposes of any resources that implement and then clears all resources + /// + public void ClearAndDisposeResources() + { + if (_resources != null) { - if (_resources != null) + foreach (var kvp in _resources) { - foreach (var kvp in _resources) - { - (kvp.Value as IDisposable)?.Dispose(); - } - - _resources.Clear(); + (kvp.Value as IDisposable)?.Dispose(); } + + _resources.Clear(); } } -} \ No newline at end of file +} diff --git a/components/Effects/src/Shadows/Effects.cs b/components/Effects/src/Shadows/Effects.cs index fa137d24..dab8a842 100644 --- a/components/Effects/src/Shadows/Effects.cs +++ b/components/Effects/src/Shadows/Effects.cs @@ -4,55 +4,54 @@ using Microsoft.UI.Xaml; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// Helper class for attaching shadows to s. +/// +public static class Effects { /// - /// Helper class for attaching shadows to s. + /// Gets the shadow attached to a by getting the value of the property. /// - public static class Effects + /// The the is attached to. + /// The that is attached to the FrameworkElement. + public static AttachedShadowBase GetShadow(FrameworkElement obj) { - /// - /// Gets the shadow attached to a by getting the value of the property. - /// - /// The the is attached to. - /// The that is attached to the FrameworkElement. - public static AttachedShadowBase GetShadow(FrameworkElement obj) + return (AttachedShadowBase)obj.GetValue(ShadowProperty); + } + + /// + /// Attaches a shadow to an element by setting the property. + /// + /// The to attach the shadow to. + /// The that will be attached to the element + public static void SetShadow(FrameworkElement obj, AttachedShadowBase value) + { + obj.SetValue(ShadowProperty, value); + } + + /// + /// Attached for setting an to a . + /// + public static readonly DependencyProperty ShadowProperty = + DependencyProperty.RegisterAttached("Shadow", typeof(AttachedShadowBase), typeof(Effects), new PropertyMetadata(null, OnShadowChanged)); + + private static void OnShadowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is FrameworkElement element)) { - return (AttachedShadowBase)obj.GetValue(ShadowProperty); + return; } - /// - /// Attaches a shadow to an element by setting the property. - /// - /// The to attach the shadow to. - /// The that will be attached to the element - public static void SetShadow(FrameworkElement obj, AttachedShadowBase value) + if (e.OldValue is AttachedShadowBase oldShadow) { - obj.SetValue(ShadowProperty, value); + oldShadow.DisconnectElement(element); } - /// - /// Attached for setting an to a . - /// - public static readonly DependencyProperty ShadowProperty = - DependencyProperty.RegisterAttached("Shadow", typeof(AttachedShadowBase), typeof(Effects), new PropertyMetadata(null, OnShadowChanged)); - - private static void OnShadowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + if (e.NewValue is AttachedShadowBase newShadow) { - if (!(d is FrameworkElement element)) - { - return; - } - - if (e.OldValue is AttachedShadowBase oldShadow) - { - oldShadow.DisconnectElement(element); - } - - if (e.NewValue is AttachedShadowBase newShadow) - { - newShadow.ConnectElement(element); - } + newShadow.ConnectElement(element); } } } diff --git a/components/Effects/src/Shadows/IAlphaMaskProvider.cs b/components/Effects/src/Shadows/IAlphaMaskProvider.cs index e1b794bf..5e1cef60 100644 --- a/components/Effects/src/Shadows/IAlphaMaskProvider.cs +++ b/components/Effects/src/Shadows/IAlphaMaskProvider.cs @@ -4,22 +4,21 @@ using Microsoft.UI.Composition; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// Any user control can implement this interface to provide a custom alpha mask to it's parent DropShadowPanel +/// +public interface IAlphaMaskProvider { /// - /// Any user control can implement this interface to provide a custom alpha mask to it's parent DropShadowPanel + /// Gets a value indicating whether the AlphaMask needs to be retrieved after the element has loaded. /// - public interface IAlphaMaskProvider - { - /// - /// Gets a value indicating whether the AlphaMask needs to be retrieved after the element has loaded. - /// - bool WaitUntilLoaded { get; } + bool WaitUntilLoaded { get; } - /// - /// This method should return the appropiate alpha mask to be used in the shadow of this control - /// - /// The alpha mask as a composition brush - CompositionBrush GetAlphaMask(); - } -} \ No newline at end of file + /// + /// This method should return the appropiate alpha mask to be used in the shadow of this control + /// + /// The alpha mask as a composition brush + CompositionBrush GetAlphaMask(); +} diff --git a/components/Effects/src/Shadows/IAttachedShadow.cs b/components/Effects/src/Shadows/IAttachedShadow.cs index a889bbe9..9eecd360 100644 --- a/components/Effects/src/Shadows/IAttachedShadow.cs +++ b/components/Effects/src/Shadows/IAttachedShadow.cs @@ -7,43 +7,42 @@ using Microsoft.UI.Xaml; using Windows.UI; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// Interface representing the common properties found within an attached shadow, for implementation. +/// +public interface IAttachedShadow { /// - /// Interface representing the common properties found within an attached shadow, for implementation. + /// Gets or sets the blur radius of the shadow. + /// + double BlurRadius { get; set; } + + /// + /// Gets or sets the opacity of the shadow. + /// + double Opacity { get; set; } + + /// + /// Gets or sets the offset of the shadow as a string representation of a . + /// + string Offset { get; set; } + + /// + /// Gets or sets the color of the shadow. + /// + Color Color { get; set; } + + /// + /// Get the associated for the specified . + /// + /// The for the element. + AttachedShadowElementContext GetElementContext(FrameworkElement element); + + /// + /// Gets an enumeration over the current list of of elements using this shared shadow definition. /// - public interface IAttachedShadow - { - /// - /// Gets or sets the blur radius of the shadow. - /// - double BlurRadius { get; set; } - - /// - /// Gets or sets the opacity of the shadow. - /// - double Opacity { get; set; } - - /// - /// Gets or sets the offset of the shadow as a string representation of a . - /// - string Offset { get; set; } - - /// - /// Gets or sets the color of the shadow. - /// - Color Color { get; set; } - - /// - /// Get the associated for the specified . - /// - /// The for the element. - AttachedShadowElementContext GetElementContext(FrameworkElement element); - - /// - /// Gets an enumeration over the current list of of elements using this shared shadow definition. - /// - /// Enumeration of objects. - IEnumerable EnumerateElementContexts(); - } + /// Enumeration of objects. + IEnumerable EnumerateElementContexts(); } diff --git a/components/Effects/src/Shadows/TypedResourceKey.cs b/components/Effects/src/Shadows/TypedResourceKey.cs index a398d62d..f210b5cd 100644 --- a/components/Effects/src/Shadows/TypedResourceKey.cs +++ b/components/Effects/src/Shadows/TypedResourceKey.cs @@ -4,32 +4,31 @@ using System; -namespace CommunityToolkit.WinUI.UI +namespace CommunityToolkit.WinUI; + +/// +/// A generic class that can be used to retrieve keyed resources of the specified type. +/// +/// The of resource the will retrieve. +internal sealed class TypedResourceKey { /// - /// A generic class that can be used to retrieve keyed resources of the specified type. + /// Initializes a new instance of the class with the specified key. /// - /// The of resource the will retrieve. - internal sealed class TypedResourceKey - { - /// - /// Initializes a new instance of the class with the specified key. - /// - /// The resource's key - public TypedResourceKey(string key) => Key = key; + /// The resource's key + public TypedResourceKey(string key) => Key = key; - /// - /// Gets the key of the resource to be retrieved. - /// - public string Key { get; } + /// + /// Gets the key of the resource to be retrieved. + /// + public string Key { get; } - /// - /// Implicit operator for transforming a string into a key. - /// - /// The key string. - public static implicit operator TypedResourceKey(string key) - { - return new TypedResourceKey(key); - } + /// + /// Implicit operator for transforming a string into a key. + /// + /// The key string. + public static implicit operator TypedResourceKey(string key) + { + return new TypedResourceKey(key); } } From 8d0c3f0d9b77644c60f4af4bb37464f75c87f8b2 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 13 Apr 2023 13:29:51 -0700 Subject: [PATCH 3/7] Initial updates to fix build for Shadow (nullability mostly) Tested on WinAppSDK + UWP (need to compare with UWP branch next though) Removed need for internal DesignTimeHelpers in Extensions (only cares about 16299 and lower which is below our min target) Added basic sample, needs to bring over more detail/doc here Added overall doc from doc repo, though needs editing/updating --- components/Effects/samples/Assets/Llama.jpg | Bin 0 -> 57164 bytes .../Effects/samples/AttachedDropShadow.md | 24 +++ .../AttachedDropShadowBasicSample.xaml | 32 ++++ .../AttachedDropShadowBasicSample.xaml.cs | 14 ++ components/Effects/samples/AttachedShadows.md | 148 ++++++++++++++++++ .../Effects/samples/Effects.Samples.csproj | 8 + components/Effects/samples/Effects.md | 30 +--- .../samples/EffectsTemplatedSample.xaml | 16 -- .../samples/EffectsTemplatedSample.xaml.cs | 21 --- .../src/CommunityToolkit.WinUI.Effects.csproj | 4 + .../Effects/src/Shadows/AttachedDropShadow.cs | 74 ++++----- .../Effects/src/Shadows/AttachedShadowBase.cs | 53 +++---- .../Shadows/AttachedShadowElementContext.cs | 73 +++++---- components/Effects/src/Shadows/Effects.cs | 2 - .../Effects/src/Shadows/IAlphaMaskProvider.cs | 4 + .../Effects/src/Shadows/IAttachedShadow.cs | 5 +- .../Effects/src/Shadows/TypedResourceKey.cs | 2 - .../tests/AttachedDropShadowTestPage.xaml | 25 +++ ....cs => AttachedDropShadowTestPage.xaml.cs} | 4 +- .../Effects/tests/AttachedDropShadowTests.cs | 66 ++++++++ .../Effects/tests/Effects.Tests.projitems | 8 +- .../Effects/tests/ExampleEffectsTestClass.cs | 133 ---------------- .../Effects/tests/ExampleEffectsTestPage.xaml | 14 -- .../src/Internal/DesignTimeHelpers.cs | 59 ------- .../Extensions/src/Media/VisualExtensions.cs | 41 +++-- 25 files changed, 460 insertions(+), 400 deletions(-) create mode 100644 components/Effects/samples/Assets/Llama.jpg create mode 100644 components/Effects/samples/AttachedDropShadow.md create mode 100644 components/Effects/samples/AttachedDropShadowBasicSample.xaml create mode 100644 components/Effects/samples/AttachedDropShadowBasicSample.xaml.cs create mode 100644 components/Effects/samples/AttachedShadows.md delete mode 100644 components/Effects/samples/EffectsTemplatedSample.xaml delete mode 100644 components/Effects/samples/EffectsTemplatedSample.xaml.cs create mode 100644 components/Effects/tests/AttachedDropShadowTestPage.xaml rename components/Effects/tests/{ExampleEffectsTestPage.xaml.cs => AttachedDropShadowTestPage.xaml.cs} (79%) create mode 100644 components/Effects/tests/AttachedDropShadowTests.cs delete mode 100644 components/Effects/tests/ExampleEffectsTestClass.cs delete mode 100644 components/Effects/tests/ExampleEffectsTestPage.xaml delete mode 100644 components/Extensions/src/Internal/DesignTimeHelpers.cs diff --git a/components/Effects/samples/Assets/Llama.jpg b/components/Effects/samples/Assets/Llama.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7fe1cffbf2ea9f1ec2818e469f1145e6b1776c4 GIT binary patch literal 57164 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­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 [!SAMPLE AttachedDropShadowBasicSample] diff --git a/components/Effects/samples/AttachedDropShadowBasicSample.xaml b/components/Effects/samples/AttachedDropShadowBasicSample.xaml new file mode 100644 index 00000000..2cab5d49 --- /dev/null +++ b/components/Effects/samples/AttachedDropShadowBasicSample.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/components/Effects/samples/AttachedDropShadowBasicSample.xaml.cs b/components/Effects/samples/AttachedDropShadowBasicSample.xaml.cs new file mode 100644 index 00000000..e8852e8d --- /dev/null +++ b/components/Effects/samples/AttachedDropShadowBasicSample.xaml.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace EffectsExperiment.Samples; + +[ToolkitSample(id: nameof(AttachedDropShadowBasicSample), "Basic Attached Drop Shadow", description: "A sample for showing how to create an AttachedDropShadow on an element.")] +public sealed partial class AttachedDropShadowBasicSample : Page +{ + public AttachedDropShadowBasicSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Effects/samples/AttachedShadows.md b/components/Effects/samples/AttachedShadows.md new file mode 100644 index 00000000..ba6a6b49 --- /dev/null +++ b/components/Effects/samples/AttachedShadows.md @@ -0,0 +1,148 @@ +--- +title: Attached Shadows +author: michael-hawker +description: Attached Shadows allow you to easily create shadow effects on elements. +keywords: windows 10, windows 11, uwp, winui, winappsdk, windows community toolkit, shadow, shadows, dropshadow, dropshadowpanel, attachedshadow, attacheddropshadow, attachedcardshadow +dev_langs: + - csharp +category: Extensions +subcategory: Media +discussion-id: 0 +issue-id: 0 +--- + +# Attached Shadows + +Attached Shadows allow you to more easily create beautiful shadow effects within your app with little to no modification of the visual tree required, unlike our previous `DropShadowPanel` control. + +> **Platform APIs:** [`AttachedCardShadow`](/dotnet/api/microsoft.toolkit.uwp.ui.media.attachedcardshadow), [`AttachedDropShadow`](/dotnet/api/microsoft.toolkit.uwp.ui.attacheddropshadow) + +## Introduction + +There are two types of attached shadows available today, the `AttachedCardShadow` and the `AttachedDropShadow`. It is recommended to use the `AttachedCardShadow` where possible, if you don't mind the dependency on Win2D. The `AttachedCardShadow` provides an easier to use experience that is more performant and easier to apply across an entire set of elements, assuming those elements are rounded-rectangular in shape. The `AttachedDropShadow` provides masking support and can be leveraged in any UWP app without adding an extra dependency. + +### Capability Comparison + +The following table outlines the various capabilities of each shadow type in addition to comparing to the previous `DropShadowPanel` implementation: + +| Capability | AttachedCardShadow | AttachedDropShadow | DropShadowPanel (deprecated) | +|-------------------------------|--------------------------------------------------------------------|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| Dependency/NuGet Package | 🟡 Win2D via
`Microsoft.Toolkit.Uwp.UI.Media` | ✅ Framework Only (Composition Effect)
`CommunityToolkit.WinUI.Effects` | ✅ Framework Only (Composition Effect)
`Microsoft.Toolkit.Uwp.UI.Controls` (legacy) | +| Layer | Inline Composition +
Win2D Clip Geometry | Composition via
Target Element Backdrop | Composition via
`ContentControl` Container | +| Modify Visual Tree | ✅ No | 🟡 Usually requires single target element, even for multiple shadows | ❌ Individually wrap each element needing shadow | +| Extra Visual Tree Depth | ✅ 0 | 🟡 1 per sibling element to cast one or more shadows to | ❌ _**4** per Shadowed Element_ | +| Masking/Geometry | 🟡 Rectangular/Rounded-Rectangles only | ✅ Can mask images, text, and shapes (performance penalty) | ✅ Can mask images, text, and shapes (performance penalty) | +| Performance | ✅ Fast, applies rectangular clipped geometry | 🟡 Slower, especially when masking (default);
can use rounded-rectangles optimization | ❌ Slowest, no optimization for rounded-rectangles | +| ResourceDictionary Support | ✅ Yes | ✅ Yes | ❌ Limited, via complete custom control style +
still need to wrap each element to apply | +| Usable in Styles | ✅ Yes, anywhere, including app-level | 🟡 Yes, but limited in scope due to element target | ❌ No | +| Supports Transparent Elements | ✅ Yes, shadow is clipped and not visible | ❌ No, shadow shows through transparent element | ❌ No, shadow shows through transparent element | +| Animation Support | ✅ Yes, in XAML via [`AnimationSet`](../animations/AnimationSet.md) | 🟡 Partial, translating/moving element may desync shadow | ❌ No | + +## AttachedCardShadow (Win2D) + +The `AttachedCardShadow` is the easiest to use and most performant shadow. It is recommended to use it where possible, if taking a Win2D dependency is not a concern. It's only drawbacks are the extra dependency required and that it only supports rectangular and rounded-rectangular geometries (as described in the table above). + +The great benefit to the `AttachedCardShadow` is that no extra surface or element is required to add the shadow. This reduces the complexity required in development and allows shadows to easily be added at any point in the development process. It also supports transparent elements, without displaying the shadow behind them! + +### Example + +The example shows how easy it is to not only apply an `AttachedCardShadow` to an element, but use it in a style to apply to multiple elements as well: + +```xaml + xmlns:ui="using:Microsoft.Toolkit.Uwp.UI" + xmlns:media="using:Microsoft.Toolkit.Uwp.UI.Media"/> + + + + + + + + +