8
8
using Windows . UI ;
9
9
using Windows . UI . Composition ;
10
10
using Windows . UI . Xaml ;
11
+ using Windows . UI . Xaml . Hosting ;
11
12
12
13
namespace Microsoft . Toolkit . Uwp . UI . Media
13
14
{
@@ -20,9 +21,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
20
21
public sealed class AttachedCardShadow : AttachedShadowBase
21
22
{
22
23
private const float MaxBlurRadius = 72 ;
23
- private static readonly TypedResourceKey < CompositionGeometricClip > ClipResourceKey = "Clip" ;
24
24
25
+ private static readonly TypedResourceKey < CompositionGeometricClip > ClipResourceKey = "Clip" ;
25
26
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" ;
26
36
private static readonly TypedResourceKey < CompositionRoundedRectangleGeometry > RoundedRectangleGeometryResourceKey = "RoundedGeometry" ;
27
37
private static readonly TypedResourceKey < CompositionSpriteShape > ShapeResourceKey = "Shape" ;
28
38
private static readonly TypedResourceKey < ShapeVisual > ShapeVisualResourceKey = "ShapeVisual" ;
@@ -39,6 +49,16 @@ public sealed class AttachedCardShadow : AttachedShadowBase
39
49
typeof ( AttachedCardShadow ) ,
40
50
new PropertyMetadata ( 4d , OnDependencyPropertyChanged ) ) ; // Default WinUI ControlCornerRadius is 4
41
51
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
+
42
62
/// <summary>
43
63
/// Gets or sets the roundness of the shadow's corners.
44
64
/// </summary>
@@ -48,24 +68,47 @@ public double CornerRadius
48
68
set => SetValue ( CornerRadiusProperty , value ) ;
49
69
}
50
70
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
+
51
80
/// <inheritdoc/>
52
81
public override bool IsSupported => SupportsCompositionVisualSurface ;
53
82
54
83
/// <inheritdoc/>
55
84
protected internal override bool SupportsOnSizeChangedEvent => true ;
56
85
86
+ /// <inheritdoc/>
87
+ protected internal override void OnElementContextInitialized ( AttachedShadowElementContext context )
88
+ {
89
+ UpdateVisualOpacityMask ( context ) ;
90
+ base . OnElementContextInitialized ( context ) ;
91
+ }
92
+
57
93
/// <inheritdoc/>
58
94
protected override void OnPropertyChanged ( AttachedShadowElementContext context , DependencyProperty property , object oldValue , object newValue )
59
95
{
60
96
if ( property == CornerRadiusProperty )
61
97
{
98
+ UpdateShadowClip ( context ) ;
99
+ UpdateVisualOpacityMask ( context ) ;
100
+
62
101
var geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ;
63
102
if ( geometry != null )
64
103
{
65
104
geometry . CornerRadius = new Vector2 ( ( float ) ( double ) newValue ) ;
66
105
}
67
-
106
+ }
107
+ else if ( property == InnerContentClipModeProperty )
108
+ {
68
109
UpdateShadowClip ( context ) ;
110
+ UpdateVisualOpacityMask ( context ) ;
111
+ SetElementChildVisual ( context ) ;
69
112
}
70
113
else
71
114
{
@@ -114,6 +157,13 @@ protected override CompositionBrush GetShadowMask(AttachedShadowElementContext c
114
157
/// <inheritdoc/>
115
158
protected override CompositionClip GetShadowClip ( AttachedShadowElementContext context )
116
159
{
160
+ if ( InnerContentClipMode != InnerContentClipMode . CompositionGeometricClip )
161
+ {
162
+ context . RemoveAndDisposeResource ( PathGeometryResourceKey ) ;
163
+ context . RemoveAndDisposeResource ( ClipResourceKey ) ;
164
+ return null ;
165
+ }
166
+
117
167
// The way this shadow works without the need to project on another element is because
118
168
// we're clipping the inner part of the shadow which would be cast on the element
119
169
// 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
144
194
return clip ;
145
195
}
146
196
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
+
147
250
/// <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 />
148
304
protected internal override void OnSizeChanged ( AttachedShadowElementContext context , Size newSize , Size previousSize )
149
305
{
150
- var sizeAsVec2 = newSize . ToVector2 ( ) ;
306
+ Vector2 sizeAsVec2 = newSize . ToVector2 ( ) ;
151
307
152
- var geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ;
308
+ CompositionRoundedRectangleGeometry geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ;
153
309
if ( geometry != null )
154
310
{
155
311
geometry . Size = sizeAsVec2 ;
156
312
}
157
313
158
- var visualSurface = context . GetResource ( VisualSurfaceResourceKey ) ;
314
+ CompositionVisualSurface visualSurface = context . GetResource ( VisualSurfaceResourceKey ) ;
159
315
if ( geometry != null )
160
316
{
161
317
visualSurface . SourceSize = sizeAsVec2 ;
162
318
}
163
319
164
- var shapeVisual = context . GetResource ( ShapeVisualResourceKey ) ;
320
+ ShapeVisual shapeVisual = context . GetResource ( ShapeVisualResourceKey ) ;
165
321
if ( geometry != null )
166
322
{
167
323
shapeVisual . Size = sizeAsVec2 ;
0 commit comments