88using Windows . UI ;
99using Windows . UI . Composition ;
1010using Windows . UI . Xaml ;
11+ using Windows . UI . Xaml . Hosting ;
1112
1213namespace 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 ;
0 commit comments