1+ // ------------------------------------------------------
2+ // Copyright (C) Microsoft. All rights reserved.
3+ // ------------------------------------------------------
4+
5+ using Microsoft . Graphics . Canvas . Geometry ;
6+ using System . Numerics ;
7+ using Windows . Foundation ;
8+ using Windows . Foundation . Metadata ;
9+ using Windows . UI ;
10+ using Windows . UI . Composition ;
11+ using Windows . UI . Xaml ;
12+
13+ namespace Microsoft . Toolkit . Uwp . UI . Media . Shadows
14+ {
15+ public class AttachedCardShadow : AttachedShadowBase
16+ {
17+ private const float MaxBlurRadius = 72 ;
18+ private static readonly TypedResourceKey < CompositionGeometricClip > ClipResourceKey = "Clip" ;
19+ private static readonly bool SupportsCompositionVisualSurface ;
20+ private static readonly bool SupportsCompositionGeometricClip ;
21+
22+ private static readonly TypedResourceKey < CompositionPathGeometry > PathGeometryResourceKey = "PathGeometry" ;
23+ private static readonly TypedResourceKey < CompositionRoundedRectangleGeometry > RoundedRectangleGeometryResourceKey = "RoundedGeometry" ;
24+ private static readonly TypedResourceKey < CompositionSpriteShape > ShapeResourceKey = "Shape" ;
25+ private static readonly TypedResourceKey < ShapeVisual > ShapeVisualResourceKey = "ShapeVisual" ;
26+ private static readonly TypedResourceKey < CompositionSurfaceBrush > SurfaceBrushResourceKey = "SurfaceBrush" ;
27+ private static readonly TypedResourceKey < CompositionVisualSurface > VisualSurfaceResourceKey = "VisualSurface" ;
28+
29+ public static readonly DependencyProperty CornerRadiusProperty =
30+ DependencyProperty . Register (
31+ nameof ( CornerRadius ) ,
32+ typeof ( double ) ,
33+ typeof ( AttachedCardShadow ) ,
34+ new PropertyMetadata ( 8d , OnDependencyPropertyChanged ) ) ;
35+
36+ static AttachedCardShadow ( )
37+ {
38+ SupportsCompositionGeometricClip = ApiInformation . IsTypePresent ( typeof ( CompositionGeometricClip ) . FullName ) ;
39+ SupportsCompositionVisualSurface = ApiInformation . IsTypePresent ( typeof ( CompositionVisualSurface ) . FullName ) ; ;
40+ }
41+
42+ /// <summary>
43+ /// Gets or sets the roundness of the shadow's corners
44+ /// </summary>
45+ public double CornerRadius
46+ {
47+ get => ( double ) GetValue ( CornerRadiusProperty ) ;
48+ set => SetValue ( CornerRadiusProperty , value ) ;
49+ }
50+
51+ /// <summary>
52+ /// <inheritdoc/>
53+ /// </summary>
54+ public override bool IsSupported => SupportsCompositionVisualSurface ;
55+
56+ /// <summary>
57+ /// <inheritdoc/>
58+ /// </summary>
59+ public override bool SupportsOnSizeChangedEvent => true ;
60+
61+ protected override void OnElementContextUninitialized ( AttachedShadowElementContext context )
62+ {
63+ context . ClearAndDisposeResources ( ) ;
64+ base . OnElementContextUninitialized ( context ) ;
65+ }
66+
67+ protected override void OnPropertyChanged ( AttachedShadowElementContext context , DependencyProperty property , object oldValue , object newValue )
68+ {
69+ if ( property == CornerRadiusProperty )
70+ {
71+ var geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ;
72+ if ( geometry != null )
73+ {
74+ geometry . CornerRadius = new Vector2 ( ( float ) ( double ) newValue ) ;
75+ }
76+
77+ UpdateShadowClip ( context ) ;
78+ }
79+ else
80+ {
81+ base . OnPropertyChanged ( context , property , oldValue , newValue ) ;
82+ }
83+ }
84+
85+ protected override CompositionBrush GetShadowMask ( AttachedShadowElementContext context )
86+ {
87+ if ( ! SupportsCompositionVisualSurface )
88+ {
89+ return null ;
90+ }
91+
92+ // Create rounded rectangle geometry and add it to a shape
93+ var geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ?? context . AddResource (
94+ RoundedRectangleGeometryResourceKey ,
95+ context . Compositor . CreateRoundedRectangleGeometry ( ) ) ;
96+ geometry . CornerRadius = new Vector2 ( ( float ) CornerRadius ) ;
97+
98+ var shape = context . GetResource ( ShapeResourceKey ) ?? context . AddResource ( ShapeResourceKey , context . Compositor . CreateSpriteShape ( geometry ) ) ;
99+ shape . FillBrush = context . Compositor . CreateColorBrush ( Colors . Black ) ;
100+
101+ // Create a ShapeVisual so that our geometry can be rendered to a visual
102+ var shapeVisual = context . GetResource ( ShapeVisualResourceKey ) ??
103+ context . AddResource ( ShapeVisualResourceKey , context . Compositor . CreateShapeVisual ( ) ) ;
104+ shapeVisual . Shapes . Add ( shape ) ;
105+
106+ // Create a CompositionVisualSurface, which renders our ShapeVisual to a texture
107+ var visualSurface = context . GetResource ( VisualSurfaceResourceKey ) ??
108+ context . AddResource ( VisualSurfaceResourceKey , context . Compositor . CreateVisualSurface ( ) ) ;
109+ visualSurface . SourceVisual = shapeVisual ;
110+
111+ // Create a CompositionSurfaceBrush to render our CompositionVisualSurface to a brush.
112+ // Now we have a rounded rectangle brush that can be used on as the mask for our shadow.
113+ var surfaceBrush = context . GetResource ( SurfaceBrushResourceKey ) ?? context . AddResource (
114+ SurfaceBrushResourceKey ,
115+ context . Compositor . CreateSurfaceBrush ( visualSurface ) ) ;
116+
117+ geometry . Size = visualSurface . SourceSize = shapeVisual . Size = context . Element . RenderSize . ToVector2 ( ) ;
118+
119+ return surfaceBrush ;
120+ }
121+
122+ protected override CompositionClip GetShadowClip ( AttachedShadowElementContext context )
123+ {
124+ var pathGeom = context . GetResource ( PathGeometryResourceKey ) ??
125+ context . AddResource ( PathGeometryResourceKey , context . Compositor . CreatePathGeometry ( ) ) ;
126+ var clip = context . GetResource ( ClipResourceKey ) ?? context . AddResource ( ClipResourceKey , context . Compositor . CreateGeometricClip ( pathGeom ) ) ;
127+
128+ // Create rounded rectangle geometry at a larger size that compensates for the size of the stroke,
129+ // as we want the inside edge of the stroke to match the edges of the element.
130+ // Additionally, the inside edge of the stroke will have a smaller radius than the radius we specified.
131+ // Using "(StrokeThickness / 2) + Radius" as our rectangle's radius will give us an inside stroke radius that matches the radius we want.
132+ var canvasRectangle = CanvasGeometry . CreateRoundedRectangle (
133+ null ,
134+ - MaxBlurRadius / 2 ,
135+ - MaxBlurRadius / 2 ,
136+ ( float ) context . Element . ActualWidth + MaxBlurRadius ,
137+ ( float ) context . Element . ActualHeight + MaxBlurRadius ,
138+ ( MaxBlurRadius / 2 ) + ( float ) CornerRadius ,
139+ ( MaxBlurRadius / 2 ) + ( float ) CornerRadius ) ;
140+
141+ var canvasStroke = canvasRectangle . Stroke ( MaxBlurRadius ) ;
142+
143+ pathGeom . Path = new CompositionPath ( canvasStroke ) ;
144+
145+ return clip ;
146+ }
147+
148+ protected override void OnSizeChanged ( AttachedShadowElementContext context , Size newSize , Size previousSize )
149+ {
150+ var sizeAsVec2 = newSize . ToVector2 ( ) ;
151+
152+ var geometry = context . GetResource ( RoundedRectangleGeometryResourceKey ) ;
153+ if ( geometry != null )
154+ {
155+ geometry . Size = sizeAsVec2 ;
156+ }
157+
158+ var visualSurface = context . GetResource ( VisualSurfaceResourceKey ) ;
159+ if ( geometry != null )
160+ {
161+ visualSurface . SourceSize = sizeAsVec2 ;
162+ }
163+
164+ var shapeVisual = context . GetResource ( ShapeVisualResourceKey ) ;
165+ if ( geometry != null )
166+ {
167+ shapeVisual . Size = sizeAsVec2 ;
168+ }
169+
170+ UpdateShadowClip ( context ) ;
171+
172+ base . OnSizeChanged ( context , newSize , previousSize ) ;
173+ }
174+ }
175+ }
0 commit comments