Skip to content

Commit cca764f

Browse files
Merge pull request #4404 from Ryken100/ryken100/feature-AttachedNineGridCardShadow
Clip inner content from AttachedCardShadow using CompositionMaskBrush
2 parents 00e9790 + 65e350b commit cca764f

File tree

4 files changed

+213
-14
lines changed

4 files changed

+213
-14
lines changed

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Shadows/AttachedShadowWin2DXaml.bind

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
CornerRadius="32"
3838
Color="@[Color:Brush:Black]"
3939
Offset="@[Offset:Vector3:4,4]"
40-
Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"/>
40+
Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"
41+
InnerContentClipMode="@[InnerContentClipMode:Enum:InnerContentClipMode.CompositionGeometricClip]"/>
4142
</ui:Effects.Shadow>
4243
</Rectangle>
4344
<!-- If you want to apply a shadow directly in your visual tree to an untemplated element
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.Toolkit.Uwp.UI.Media
12+
{
13+
/// <summary>
14+
/// The method that each instance of <see cref="AttachedCardShadow"/> uses when clipping its inner content.
15+
/// </summary>
16+
public enum InnerContentClipMode
17+
{
18+
/// <summary>
19+
/// Do not clip inner content.
20+
/// </summary>
21+
None,
22+
23+
/// <summary>
24+
/// Use <see cref="Windows.UI.Composition.CompositionMaskBrush"/> to clip inner content.
25+
/// </summary>
26+
/// <remarks>
27+
/// This mode has better performance than <see cref="CompositionGeometricClip"/>.
28+
/// </remarks>
29+
CompositionMaskBrush,
30+
31+
/// <summary>
32+
/// Use <see cref="Windows.UI.Composition.CompositionGeometricClip"/> to clip inner content.
33+
/// </summary>
34+
/// <remarks>
35+
/// Content clipped in this mode will have smoother corners than when using <see cref="CompositionMaskBrush"/>.
36+
/// </remarks>
37+
CompositionGeometricClip
38+
}
39+
}

Microsoft.Toolkit.Uwp.UI.Media/Shadows/AttachedCardShadow.cs

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Windows.UI;
99
using Windows.UI.Composition;
1010
using Windows.UI.Xaml;
11+
using Windows.UI.Xaml.Hosting;
1112

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

25+
private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";
2526
private static readonly TypedResourceKey<CompositionPathGeometry> PathGeometryResourceKey = "PathGeometry";
27+
private static readonly TypedResourceKey<CompositionMaskBrush> OpacityMaskBrushResourceKey = "OpacityMask";
28+
private static readonly TypedResourceKey<ShapeVisual> OpacityMaskShapeVisualResourceKey = "OpacityMaskShapeVisual";
29+
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> OpacityMaskGeometryResourceKey = "OpacityMaskGeometry";
30+
private static readonly TypedResourceKey<CompositionSpriteShape> OpacityMaskSpriteShapeResourceKey = "OpacityMaskSpriteShape";
31+
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskShapeVisualSurfaceResourceKey = "OpacityMaskShapeVisualSurface";
32+
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskShapeVisualSurfaceBrushResourceKey = "OpacityMaskShapeVisualSurfaceBrush";
33+
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskVisualSurfaceResourceKey = "OpacityMaskVisualSurface";
34+
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskSurfaceBrushResourceKey = "OpacityMaskSurfaceBrush";
35+
private static readonly TypedResourceKey<SpriteVisual> OpacityMaskVisualResourceKey = "OpacityMaskVisual";
2636
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> RoundedRectangleGeometryResourceKey = "RoundedGeometry";
2737
private static readonly TypedResourceKey<CompositionSpriteShape> ShapeResourceKey = "Shape";
2838
private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual";
@@ -39,6 +49,16 @@ public sealed class AttachedCardShadow : AttachedShadowBase
3949
typeof(AttachedCardShadow),
4050
new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4
4151

52+
/// <summary>
53+
/// The <see cref="DependencyProperty"/> for <see cref="InnerContentClipMode"/>.
54+
/// </summary>
55+
public static readonly DependencyProperty InnerContentClipModeProperty =
56+
DependencyProperty.Register(
57+
nameof(InnerContentClipMode),
58+
typeof(InnerContentClipMode),
59+
typeof(AttachedCardShadow),
60+
new PropertyMetadata(InnerContentClipMode.CompositionGeometricClip, OnDependencyPropertyChanged));
61+
4262
/// <summary>
4363
/// Gets or sets the roundness of the shadow's corners.
4464
/// </summary>
@@ -48,24 +68,47 @@ public double CornerRadius
4868
set => SetValue(CornerRadiusProperty, value);
4969
}
5070

71+
/// <summary>
72+
/// Gets or sets the mode use to clip inner content from the shadow.
73+
/// </summary>
74+
public InnerContentClipMode InnerContentClipMode
75+
{
76+
get => (InnerContentClipMode)GetValue(InnerContentClipModeProperty);
77+
set => SetValue(InnerContentClipModeProperty, value);
78+
}
79+
5180
/// <inheritdoc/>
5281
public override bool IsSupported => SupportsCompositionVisualSurface;
5382

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

86+
/// <inheritdoc/>
87+
protected internal override void OnElementContextInitialized(AttachedShadowElementContext context)
88+
{
89+
UpdateVisualOpacityMask(context);
90+
base.OnElementContextInitialized(context);
91+
}
92+
5793
/// <inheritdoc/>
5894
protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue)
5995
{
6096
if (property == CornerRadiusProperty)
6197
{
98+
UpdateShadowClip(context);
99+
UpdateVisualOpacityMask(context);
100+
62101
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
63102
if (geometry != null)
64103
{
65104
geometry.CornerRadius = new Vector2((float)(double)newValue);
66105
}
67-
106+
}
107+
else if (property == InnerContentClipModeProperty)
108+
{
68109
UpdateShadowClip(context);
110+
UpdateVisualOpacityMask(context);
111+
SetElementChildVisual(context);
69112
}
70113
else
71114
{
@@ -114,6 +157,13 @@ protected override CompositionBrush GetShadowMask(AttachedShadowElementContext c
114157
/// <inheritdoc/>
115158
protected override CompositionClip GetShadowClip(AttachedShadowElementContext context)
116159
{
160+
if (InnerContentClipMode != InnerContentClipMode.CompositionGeometricClip)
161+
{
162+
context.RemoveAndDisposeResource(PathGeometryResourceKey);
163+
context.RemoveAndDisposeResource(ClipResourceKey);
164+
return null;
165+
}
166+
117167
// The way this shadow works without the need to project on another element is because
118168
// we're clipping the inner part of the shadow which would be cast on the element
119169
// itself away. This method is creating an outline so that we are only showing the
@@ -144,24 +194,130 @@ protected override CompositionClip GetShadowClip(AttachedShadowElementContext co
144194
return clip;
145195
}
146196

197+
/// <summary>
198+
/// Updates the <see cref="CompositionBrush"/> used to mask <paramref name="context"/>.<see cref="AttachedShadowElementContext.SpriteVisual">SpriteVisual</see>.
199+
/// </summary>
200+
/// <param name="context">The <see cref="AttachedShadowElementContext"/> whose <see cref="SpriteVisual"/> will be masked.</param>
201+
private void UpdateVisualOpacityMask(AttachedShadowElementContext context)
202+
{
203+
if (InnerContentClipMode != InnerContentClipMode.CompositionMaskBrush)
204+
{
205+
context.RemoveAndDisposeResource(OpacityMaskShapeVisualResourceKey);
206+
context.RemoveAndDisposeResource(OpacityMaskGeometryResourceKey);
207+
context.RemoveAndDisposeResource(OpacityMaskSpriteShapeResourceKey);
208+
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceResourceKey);
209+
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceBrushResourceKey);
210+
return;
211+
}
212+
213+
// Create ShapeVisual, and CompositionSpriteShape with geometry, these will provide the visuals for the opacity mask.
214+
ShapeVisual shapeVisual = context.GetResource(OpacityMaskShapeVisualResourceKey) ??
215+
context.AddResource(OpacityMaskShapeVisualResourceKey, context.Compositor.CreateShapeVisual());
216+
217+
CompositionRoundedRectangleGeometry geometry = context.GetResource(OpacityMaskGeometryResourceKey) ??
218+
context.AddResource(OpacityMaskGeometryResourceKey, context.Compositor.CreateRoundedRectangleGeometry());
219+
CompositionSpriteShape shape = context.GetResource(OpacityMaskSpriteShapeResourceKey) ??
220+
context.AddResource(OpacityMaskSpriteShapeResourceKey, context.Compositor.CreateSpriteShape(geometry));
221+
222+
// Set the attributes of the geometry, and add the CompositionSpriteShape to the ShapeVisual.
223+
// The geometry will have a thick outline and no fill, meaning that when used as a mask,
224+
// the shadow will only be rendered on the outer area covered by the outline, clipping out its inner portion.
225+
geometry.Offset = new Vector2(MaxBlurRadius / 2);
226+
geometry.CornerRadius = new Vector2((MaxBlurRadius / 2) + (float)CornerRadius);
227+
shape.StrokeThickness = MaxBlurRadius;
228+
shape.StrokeBrush = shape.StrokeBrush ?? context.Compositor.CreateColorBrush(Colors.Black);
229+
230+
if (!shapeVisual.Shapes.Contains(shape))
231+
{
232+
shapeVisual.Shapes.Add(shape);
233+
}
234+
235+
// Create CompositionVisualSurface using the ShapeVisual as the source visual.
236+
CompositionVisualSurface visualSurface = context.GetResource(OpacityMaskShapeVisualSurfaceResourceKey) ??
237+
context.AddResource(OpacityMaskShapeVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
238+
visualSurface.SourceVisual = shapeVisual;
239+
240+
geometry.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius);
241+
shapeVisual.Size = visualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);
242+
243+
// Create a CompositionSurfaceBrush using the CompositionVisualSurface as the source, this essentially converts the ShapeVisual into a brush.
244+
// This brush can then be used as a mask.
245+
CompositionSurfaceBrush opacityMask = context.GetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey) ??
246+
context.AddResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
247+
opacityMask.Surface = visualSurface;
248+
}
249+
147250
/// <inheritdoc/>
251+
protected override void SetElementChildVisual(AttachedShadowElementContext context)
252+
{
253+
if (context.TryGetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, out CompositionSurfaceBrush opacityMask))
254+
{
255+
// If the resource for OpacityMaskShapeVisualSurfaceBrushResourceKey exists it means this.InnerContentClipMode == CompositionVisualSurface,
256+
// which means we need to take some steps to set up an opacity mask.
257+
258+
// Create a CompositionVisualSurface, and use the SpriteVisual containing the shadow as the source.
259+
CompositionVisualSurface shadowVisualSurface = context.GetResource(OpacityMaskVisualSurfaceResourceKey) ??
260+
context.AddResource(OpacityMaskVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
261+
shadowVisualSurface.SourceVisual = context.SpriteVisual;
262+
context.SpriteVisual.RelativeSizeAdjustment = Vector2.Zero;
263+
context.SpriteVisual.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight);
264+
265+
// Adjust the offset and size of the CompositionVisualSurface to accommodate the thick outline of the shape created in UpdateVisualOpacityMask().
266+
shadowVisualSurface.SourceOffset = new Vector2(-MaxBlurRadius);
267+
shadowVisualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);
268+
269+
// Create a CompositionSurfaceBrush from the CompositionVisualSurface. This allows us to render the shadow in a brush.
270+
CompositionSurfaceBrush shadowSurfaceBrush = context.GetResource(OpacityMaskSurfaceBrushResourceKey) ??
271+
context.AddResource(OpacityMaskSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
272+
shadowSurfaceBrush.Surface = shadowVisualSurface;
273+
shadowSurfaceBrush.Stretch = CompositionStretch.None;
274+
275+
// Create a CompositionMaskBrush, using the CompositionSurfaceBrush of the shadow as the source,
276+
// and the CompositionSurfaceBrush created in UpdateVisualOpacityMask() as the mask.
277+
// This creates a brush that renders the shadow with its inner portion clipped out.
278+
CompositionMaskBrush maskBrush = context.GetResource(OpacityMaskBrushResourceKey) ??
279+
context.AddResource(OpacityMaskBrushResourceKey, context.Compositor.CreateMaskBrush());
280+
maskBrush.Source = shadowSurfaceBrush;
281+
maskBrush.Mask = opacityMask;
282+
283+
// Create a SpriteVisual and set its brush to the CompositionMaskBrush created in the previous step,
284+
// then set it as the child of the element in the context.
285+
SpriteVisual visual = context.GetResource(OpacityMaskVisualResourceKey) ??
286+
context.AddResource(OpacityMaskVisualResourceKey, context.Compositor.CreateSpriteVisual());
287+
visual.RelativeSizeAdjustment = Vector2.One;
288+
visual.Offset = new Vector3(-MaxBlurRadius, -MaxBlurRadius, 0);
289+
visual.Size = new Vector2(MaxBlurRadius * 2);
290+
visual.Brush = maskBrush;
291+
ElementCompositionPreview.SetElementChildVisual(context.Element, visual);
292+
}
293+
else
294+
{
295+
base.SetElementChildVisual(context);
296+
context.RemoveAndDisposeResource(OpacityMaskVisualSurfaceResourceKey);
297+
context.RemoveAndDisposeResource(OpacityMaskSurfaceBrushResourceKey);
298+
context.RemoveAndDisposeResource(OpacityMaskVisualResourceKey);
299+
context.RemoveAndDisposeResource(OpacityMaskBrushResourceKey);
300+
}
301+
}
302+
303+
/// <inheritdoc />
148304
protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize)
149305
{
150-
var sizeAsVec2 = newSize.ToVector2();
306+
Vector2 sizeAsVec2 = newSize.ToVector2();
151307

152-
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
308+
CompositionRoundedRectangleGeometry geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
153309
if (geometry != null)
154310
{
155311
geometry.Size = sizeAsVec2;
156312
}
157313

158-
var visualSurface = context.GetResource(VisualSurfaceResourceKey);
314+
CompositionVisualSurface visualSurface = context.GetResource(VisualSurfaceResourceKey);
159315
if (geometry != null)
160316
{
161317
visualSurface.SourceSize = sizeAsVec2;
162318
}
163319

164-
var shapeVisual = context.GetResource(ShapeVisualResourceKey);
320+
ShapeVisual shapeVisual = context.GetResource(ShapeVisualResourceKey);
165321
if (geometry != null)
166322
{
167323
shapeVisual.Size = sizeAsVec2;

Microsoft.Toolkit.Uwp.UI/Shadows/AttachedShadowElementContext.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,13 @@ private void Uninitialize()
137137

138138
Parent.OnElementContextUninitialized(this);
139139

140-
SpriteVisual.Shadow = null;
141-
SpriteVisual.Dispose();
140+
if (SpriteVisual != null)
141+
{
142+
SpriteVisual.Shadow = null;
143+
SpriteVisual.Dispose();
144+
}
142145

143-
Shadow.Dispose();
146+
Shadow?.Dispose();
144147

145148
ElementCompositionPreview.SetElementChildVisual(Element, null);
146149

@@ -197,7 +200,7 @@ public T AddResource<T>(string key, T resource)
197200
/// <returns>True if the resource exists, false otherwise</returns>
198201
public bool TryGetResource<T>(string key, out T resource)
199202
{
200-
if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource)
203+
if (_resources is not null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource)
201204
{
202205
resource = tResource;
203206
return true;
@@ -231,7 +234,7 @@ public T GetResource<T>(string key)
231234
/// <returns>The resource that was removed, if any</returns>
232235
public T RemoveResource<T>(string key)
233236
{
234-
if (_resources.TryGetValue(key, out var objResource))
237+
if (_resources is not null && _resources.TryGetValue(key, out var objResource))
235238
{
236239
_resources.Remove(key);
237240
if (objResource is T resource)
@@ -252,7 +255,7 @@ public T RemoveResource<T>(string key)
252255
public T RemoveAndDisposeResource<T>(string key)
253256
where T : IDisposable
254257
{
255-
if (_resources.TryGetValue(key, out var objResource))
258+
if (_resources is not null && _resources.TryGetValue(key, out var objResource))
256259
{
257260
_resources.Remove(key);
258261
if (objResource is T resource)
@@ -306,7 +309,7 @@ internal T RemoveAndDisposeResource<T>(TypedResourceKey<T> key)
306309
/// </summary>
307310
public void ClearAndDisposeResources()
308311
{
309-
if (_resources != null)
312+
if (_resources is not null)
310313
{
311314
foreach (var kvp in _resources)
312315
{

0 commit comments

Comments
 (0)