Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
CornerRadius="32"
Color="@[Color:Brush:Black]"
Offset="@[Offset:Vector3:4,4]"
Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"/>
Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"
InnerContentClipMode="@[InnerContentClipMode:Enum:InnerContentClipMode.CompositionGeometricClip]"/>
</ui:Effects.Shadow>
</Rectangle>
<!-- If you want to apply a shadow directly in your visual tree to an untemplated element
Expand Down
39 changes: 39 additions & 0 deletions Microsoft.Toolkit.Uwp.UI.Media/Enums/InnerContentClipMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Toolkit.Uwp.UI.Media
{
/// <summary>
/// The method that each instance of <see cref="AttachedCardShadow"/> uses when clipping its inner content.
/// </summary>
public enum InnerContentClipMode
{
/// <summary>
/// Do not clip inner content.
/// </summary>
None,

/// <summary>
/// Use <see cref="Windows.UI.Composition.CompositionMaskBrush"/> to clip inner content.
/// </summary>
/// <remarks>
/// This mode has better performance than <see cref="CompositionGeometricClip"/>.
/// </remarks>
CompositionMaskBrush,

/// <summary>
/// Use <see cref="Windows.UI.Composition.CompositionGeometricClip"/> to clip inner content.
/// </summary>
/// <remarks>
/// Content clipped in this mode will have smoother corners than when using <see cref="CompositionMaskBrush"/>.
/// </remarks>
CompositionGeometricClip
}
}
145 changes: 143 additions & 2 deletions Microsoft.Toolkit.Uwp.UI.Media/Shadows/AttachedCardShadow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Hosting;

namespace Microsoft.Toolkit.Uwp.UI.Media
{
Expand All @@ -20,9 +21,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
public sealed class AttachedCardShadow : AttachedShadowBase
{
private const float MaxBlurRadius = 72;
private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";

private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";
private static readonly TypedResourceKey<CompositionPathGeometry> PathGeometryResourceKey = "PathGeometry";
private static readonly TypedResourceKey<CompositionMaskBrush> OpacityMaskBrushResourceKey = "OpacityMask";
private static readonly TypedResourceKey<ShapeVisual> OpacityMaskShapeVisualResourceKey = "OpacityMaskShapeVisual";
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> OpacityMaskGeometryResourceKey = "OpacityMaskGeometry";
private static readonly TypedResourceKey<CompositionSpriteShape> OpacityMaskSpriteShapeResourceKey = "OpacityMaskSpriteShape";
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskShapeVisualSurfaceResourceKey = "OpacityMaskShapeVisualSurface";
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskShapeVisualSurfaceBrushResourceKey = "OpacityMaskShapeVisualSurfaceBrush";
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskVisualSurfaceResourceKey = "OpacityMaskVisualSurface";
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskSurfaceBrushResourceKey = "OpacityMaskSurfaceBrush";
private static readonly TypedResourceKey<SpriteVisual> OpacityMaskVisualResourceKey = "OpacityMaskVisual";
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> RoundedRectangleGeometryResourceKey = "RoundedGeometry";
private static readonly TypedResourceKey<CompositionSpriteShape> ShapeResourceKey = "Shape";
private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual";
Expand All @@ -39,6 +49,16 @@ public sealed class AttachedCardShadow : AttachedShadowBase
typeof(AttachedCardShadow),
new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4

/// <summary>
/// The <see cref="DependencyProperty"/> for <see cref="InnerContentClipMode"/>.
/// </summary>
public static readonly DependencyProperty InnerContentClipModeProperty =
DependencyProperty.Register(
nameof(InnerContentClipMode),
typeof(InnerContentClipMode),
typeof(AttachedCardShadow),
new PropertyMetadata(InnerContentClipMode.CompositionGeometricClip, OnDependencyPropertyChanged));

/// <summary>
/// Gets or sets the roundness of the shadow's corners.
/// </summary>
Expand All @@ -48,24 +68,47 @@ public double CornerRadius
set => SetValue(CornerRadiusProperty, value);
}

/// <summary>
/// Gets or sets the mode use to clip inner content from the shadow.
/// </summary>
public InnerContentClipMode InnerContentClipMode
{
get => (InnerContentClipMode)GetValue(InnerContentClipModeProperty);
set => SetValue(InnerContentClipModeProperty, value);
}

/// <inheritdoc/>
public override bool IsSupported => SupportsCompositionVisualSurface;

/// <inheritdoc/>
protected internal override bool SupportsOnSizeChangedEvent => true;

/// <inheritdoc/>
protected internal override void OnElementContextInitialized(AttachedShadowElementContext context)
{
UpdateVisualOpacityMask(context);
base.OnElementContextInitialized(context);
}

/// <inheritdoc/>
protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue)
{
if (property == CornerRadiusProperty)
{
UpdateShadowClip(context);
UpdateVisualOpacityMask(context);

var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
if (geometry != null)
{
geometry.CornerRadius = new Vector2((float)(double)newValue);
}

}
else if (property == InnerContentClipModeProperty)
{
UpdateShadowClip(context);
UpdateVisualOpacityMask(context);
SetElementChildVisual(context);
}
else
{
Expand Down Expand Up @@ -114,6 +157,13 @@ protected override CompositionBrush GetShadowMask(AttachedShadowElementContext c
/// <inheritdoc/>
protected override CompositionClip GetShadowClip(AttachedShadowElementContext context)
{
if (InnerContentClipMode != InnerContentClipMode.CompositionGeometricClip)
{
context.RemoveAndDisposeResource(PathGeometryResourceKey);
context.RemoveAndDisposeResource(ClipResourceKey);
return null;
}

// The way this shadow works without the need to project on another element is because
// we're clipping the inner part of the shadow which would be cast on the element
// itself away. This method is creating an outline so that we are only showing the
Expand Down Expand Up @@ -144,7 +194,98 @@ protected override CompositionClip GetShadowClip(AttachedShadowElementContext co
return clip;
}

/// <summary>
/// Updates the <see cref="CompositionBrush"/> used to mask <paramref name="context"/>.<see cref="AttachedShadowElementContext.SpriteVisual">SpriteVisual</see>.
/// </summary>
/// <param name="context">The <see cref="AttachedShadowElementContext"/> whose <see cref="SpriteVisual"/> will be masked.</param>
private void UpdateVisualOpacityMask(AttachedShadowElementContext context)
{
if (InnerContentClipMode != InnerContentClipMode.CompositionMaskBrush)
{
context.RemoveAndDisposeResource(OpacityMaskShapeVisualResourceKey);
context.RemoveAndDisposeResource(OpacityMaskGeometryResourceKey);
context.RemoveAndDisposeResource(OpacityMaskSpriteShapeResourceKey);
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceResourceKey);
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceBrushResourceKey);
return;
}

// Create a rounded rectangle Visual with a thick outline and no fill, then use a VisualSurface of it as an opacity mask for the shadow.
// This will have the effect of clipping the inner content of the shadow, so that the casting element is not covered by the shadow,
// while the shadow is still rendered outside of the element. Similar to what takes place in GetVisualClip,
// except here we use a brush to mask content instead of a pure geometric clip.
var shapeVisual = context.GetResource(OpacityMaskShapeVisualResourceKey) ??
context.AddResource(OpacityMaskShapeVisualResourceKey, context.Compositor.CreateShapeVisual());

CompositionRoundedRectangleGeometry geom = context.GetResource(OpacityMaskGeometryResourceKey) ??
context.AddResource(OpacityMaskGeometryResourceKey, context.Compositor.CreateRoundedRectangleGeometry());
CompositionSpriteShape shape = context.GetResource(OpacityMaskSpriteShapeResourceKey) ??
context.AddResource(OpacityMaskSpriteShapeResourceKey, context.Compositor.CreateSpriteShape(geom));

geom.Offset = new Vector2(MaxBlurRadius / 2);
geom.CornerRadius = new Vector2((MaxBlurRadius / 2) + (float)CornerRadius);
shape.StrokeThickness = MaxBlurRadius;
shape.StrokeBrush = shape.StrokeBrush ?? context.Compositor.CreateColorBrush(Colors.Black);

if (!shapeVisual.Shapes.Contains(shape))
{
shapeVisual.Shapes.Add(shape);
}

var visualSurface = context.GetResource(OpacityMaskShapeVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
visualSurface.SourceVisual = shapeVisual;

geom.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius);
shapeVisual.Size = visualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);

var surfaceBrush = context.GetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
surfaceBrush.Surface = visualSurface;
}

/// <inheritdoc/>
protected override void SetElementChildVisual(AttachedShadowElementContext context)
{
if (context.TryGetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, out var opacityMask))
{
var visualSurface = context.GetResource(OpacityMaskVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
visualSurface.SourceVisual = context.SpriteVisual;
context.SpriteVisual.RelativeSizeAdjustment = Vector2.Zero;
context.SpriteVisual.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight);
visualSurface.SourceOffset = new Vector2(-MaxBlurRadius);
visualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);

var surfaceBrush = context.GetResource(OpacityMaskSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
surfaceBrush.Surface = visualSurface;
surfaceBrush.Stretch = CompositionStretch.None;

CompositionMaskBrush maskBrush = context.GetResource(OpacityMaskBrushResourceKey) ??
context.AddResource(OpacityMaskBrushResourceKey, context.Compositor.CreateMaskBrush());
maskBrush.Source = surfaceBrush;
maskBrush.Mask = opacityMask;

var visual = context.GetResource(OpacityMaskVisualResourceKey) ??
context.AddResource(OpacityMaskVisualResourceKey, context.Compositor.CreateSpriteVisual());
visual.RelativeSizeAdjustment = Vector2.One;
visual.Offset = new Vector3(-MaxBlurRadius, -MaxBlurRadius, 0);
visual.Size = new Vector2(MaxBlurRadius * 2);
visual.Brush = maskBrush;
ElementCompositionPreview.SetElementChildVisual(context.Element, visual);
}
else
{
base.SetElementChildVisual(context);
context.RemoveAndDisposeResource(OpacityMaskVisualSurfaceResourceKey);
context.RemoveAndDisposeResource(OpacityMaskSurfaceBrushResourceKey);
context.RemoveAndDisposeResource(OpacityMaskVisualResourceKey);
context.RemoveAndDisposeResource(OpacityMaskBrushResourceKey);
}
}

/// <inheritdoc />
protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize)
{
var sizeAsVec2 = newSize.ToVector2();
Expand Down
13 changes: 8 additions & 5 deletions Microsoft.Toolkit.Uwp.UI/Shadows/AttachedShadowElementContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,13 @@ private void Uninitialize()

Parent.OnElementContextUninitialized(this);

SpriteVisual.Shadow = null;
SpriteVisual.Dispose();
if (SpriteVisual != null)
{
SpriteVisual.Shadow = null;
SpriteVisual.Dispose();
}

Shadow.Dispose();
Shadow?.Dispose();

ElementCompositionPreview.SetElementChildVisual(Element, null);

Expand Down Expand Up @@ -231,7 +234,7 @@ public T GetResource<T>(string key)
/// <returns>The resource that was removed, if any</returns>
public T RemoveResource<T>(string key)
{
if (_resources.TryGetValue(key, out var objResource))
if (_resources != null && _resources.TryGetValue(key, out var objResource))
{
_resources.Remove(key);
if (objResource is T resource)
Expand All @@ -252,7 +255,7 @@ public T RemoveResource<T>(string key)
public T RemoveAndDisposeResource<T>(string key)
where T : IDisposable
{
if (_resources.TryGetValue(key, out var objResource))
if (_resources != null && _resources.TryGetValue(key, out var objResource))
{
_resources.Remove(key);
if (objResource is T resource)
Expand Down