Skip to content

Clip inner content from AttachedCardShadow using CompositionMaskBrush #4404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all 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
}
}
168 changes: 162 additions & 6 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,24 +194,130 @@ 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 ShapeVisual, and CompositionSpriteShape with geometry, these will provide the visuals for the opacity mask.
ShapeVisual shapeVisual = context.GetResource(OpacityMaskShapeVisualResourceKey) ??
context.AddResource(OpacityMaskShapeVisualResourceKey, context.Compositor.CreateShapeVisual());

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

// Set the attributes of the geometry, and add the CompositionSpriteShape to the ShapeVisual.
// The geometry will have a thick outline and no fill, meaning that when used as a mask,
// the shadow will only be rendered on the outer area covered by the outline, clipping out its inner portion.
geometry.Offset = new Vector2(MaxBlurRadius / 2);
geometry.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);
}

// Create CompositionVisualSurface using the ShapeVisual as the source visual.
CompositionVisualSurface visualSurface = context.GetResource(OpacityMaskShapeVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
visualSurface.SourceVisual = shapeVisual;

geometry.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);

// Create a CompositionSurfaceBrush using the CompositionVisualSurface as the source, this essentially converts the ShapeVisual into a brush.
// This brush can then be used as a mask.
CompositionSurfaceBrush opacityMask = context.GetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
opacityMask.Surface = visualSurface;
}

/// <inheritdoc/>
protected override void SetElementChildVisual(AttachedShadowElementContext context)
{
if (context.TryGetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, out CompositionSurfaceBrush opacityMask))
{
// If the resource for OpacityMaskShapeVisualSurfaceBrushResourceKey exists it means this.InnerContentClipMode == CompositionVisualSurface,
// which means we need to take some steps to set up an opacity mask.

// Create a CompositionVisualSurface, and use the SpriteVisual containing the shadow as the source.
CompositionVisualSurface shadowVisualSurface = context.GetResource(OpacityMaskVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
shadowVisualSurface.SourceVisual = context.SpriteVisual;
context.SpriteVisual.RelativeSizeAdjustment = Vector2.Zero;
context.SpriteVisual.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight);

// Adjust the offset and size of the CompositionVisualSurface to accommodate the thick outline of the shape created in UpdateVisualOpacityMask().
shadowVisualSurface.SourceOffset = new Vector2(-MaxBlurRadius);
shadowVisualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);

// Create a CompositionSurfaceBrush from the CompositionVisualSurface. This allows us to render the shadow in a brush.
CompositionSurfaceBrush shadowSurfaceBrush = context.GetResource(OpacityMaskSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
shadowSurfaceBrush.Surface = shadowVisualSurface;
shadowSurfaceBrush.Stretch = CompositionStretch.None;

// Create a CompositionMaskBrush, using the CompositionSurfaceBrush of the shadow as the source,
// and the CompositionSurfaceBrush created in UpdateVisualOpacityMask() as the mask.
// This creates a brush that renders the shadow with its inner portion clipped out.
CompositionMaskBrush maskBrush = context.GetResource(OpacityMaskBrushResourceKey) ??
context.AddResource(OpacityMaskBrushResourceKey, context.Compositor.CreateMaskBrush());
maskBrush.Source = shadowSurfaceBrush;
maskBrush.Mask = opacityMask;

// Create a SpriteVisual and set its brush to the CompositionMaskBrush created in the previous step,
// then set it as the child of the element in the context.
SpriteVisual 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();
Vector2 sizeAsVec2 = newSize.ToVector2();

var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
CompositionRoundedRectangleGeometry geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
if (geometry != null)
{
geometry.Size = sizeAsVec2;
}

var visualSurface = context.GetResource(VisualSurfaceResourceKey);
CompositionVisualSurface visualSurface = context.GetResource(VisualSurfaceResourceKey);
if (geometry != null)
{
visualSurface.SourceSize = sizeAsVec2;
}

var shapeVisual = context.GetResource(ShapeVisualResourceKey);
ShapeVisual shapeVisual = context.GetResource(ShapeVisualResourceKey);
if (geometry != null)
{
shapeVisual.Size = sizeAsVec2;
Expand Down
17 changes: 10 additions & 7 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 @@ -197,7 +200,7 @@ public T AddResource<T>(string key, T resource)
/// <returns>True if the resource exists, false otherwise</returns>
public bool TryGetResource<T>(string key, out T resource)
{
if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource)
if (_resources is not null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource)
{
resource = tResource;
return true;
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 is not 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 is not null && _resources.TryGetValue(key, out var objResource))
{
_resources.Remove(key);
if (objResource is T resource)
Expand Down Expand Up @@ -306,7 +309,7 @@ internal T RemoveAndDisposeResource<T>(TypedResourceKey<T> key)
/// </summary>
public void ClearAndDisposeResources()
{
if (_resources != null)
if (_resources is not null)
{
foreach (var kvp in _resources)
{
Expand Down