From 20f6f886f9a7b0a7b2fbff029a5e75e3486986bb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 03:08:05 +0300 Subject: [PATCH] box-shadow support --- samples/RenderDemo/Pages/AnimationsPage.xaml | 19 ++++ src/Avalonia.Controls/Border.cs | 23 ++++- .../Presenters/ContentPresenter.cs | 19 +++- .../Utils/BorderRenderHelper.cs | 10 ++- .../Animation/Animators/BoxShadowAnimator.cs | 21 +++++ src/Avalonia.Visuals/Media/BoxShadow.cs | 88 +++++++++++++++++++ src/Avalonia.Visuals/Media/Color.cs | 44 ++++++++++ src/Avalonia.Visuals/Media/DrawingContext.cs | 8 +- .../Platform/IDrawingContextImpl.cs | 7 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 13 +-- .../Rendering/SceneGraph/GeometryNode.cs | 23 +++-- .../Rendering/SceneGraph/RectangleNode.cs | 14 ++- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 67 +++++++++++++- .../Media/DrawingContextImpl.cs | 6 +- .../Rendering/DeferredRendererTests.cs | 8 +- .../DeferredDrawingContextImplTests.cs | 8 +- .../SceneGraph/DrawOperationTests.cs | 2 +- 17 files changed, 337 insertions(+), 43 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs create mode 100644 src/Avalonia.Visuals/Media/BoxShadow.cs diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index c5ad1fbfc8..5f8b408622 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -134,6 +134,23 @@ + @@ -152,6 +169,8 @@ + + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 993528a12a..d60a41c543 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -33,6 +33,12 @@ namespace Avalonia.Controls public static readonly StyledProperty CornerRadiusProperty = AvaloniaProperty.Register(nameof(CornerRadius)); + /// + /// Defines the property. + /// + public static readonly StyledProperty BoxShadowProperty = + AvaloniaProperty.Register(nameof(BoxShadow)); + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); /// @@ -44,7 +50,8 @@ namespace Avalonia.Controls BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, - CornerRadiusProperty); + CornerRadiusProperty, + BoxShadowProperty); AffectsMeasure(BorderThicknessProperty); } @@ -83,14 +90,24 @@ namespace Avalonia.Controls get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } } - + + /// + /// Gets or sets the box shadow effect parameters + /// + public BoxShadow BoxShadow + { + get => GetValue(BoxShadowProperty); + set => SetValue(BoxShadowProperty, value); + } + /// /// Renders the control. /// /// The drawing context. public override void Render(DrawingContext context) { - _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush); + _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + BoxShadow); } /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 55ed91893c..6f3aa90d09 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); - + /// + /// Defines the property. + /// + public static readonly StyledProperty BoxShadowProperty = + Border.BoxShadowProperty.AddOwner(); + /// /// Defines the property. /// @@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters set { SetValue(CornerRadiusProperty, value); } } + /// + /// Gets or sets the box shadow effect parameters + /// + public BoxShadow BoxShadow + { + get => GetValue(BoxShadowProperty); + set => SetValue(BoxShadowProperty, value); + } + /// /// Gets the control displayed by the presenter. /// @@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters /// public override void Render(DrawingContext context) { - _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush); + _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + BoxShadow); } /// diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 126f3e35ca..63f0dc1015 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Platform; namespace Avalonia.Controls.Utils { @@ -67,14 +68,17 @@ namespace Avalonia.Controls.Utils } } - public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush) + public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, + IBrush borderBrush, BoxShadow boxShadow) { if (_useComplexRendering) { var backgroundGeometry = _backgroundGeometryCache; if (backgroundGeometry != null) { - context.DrawGeometry(background, null, backgroundGeometry); + // We are using platform impl here because I'm not sure if we should + // make the box shadow for geometries to be public API + context.PlatformImpl.DrawGeometry(background, null, backgroundGeometry.PlatformImpl, boxShadow); } var borderGeometry = _borderGeometryCache; @@ -97,7 +101,7 @@ namespace Avalonia.Controls.Utils var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness); - context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft); + context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft, boxShadow); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs new file mode 100644 index 0000000000..fced815b85 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs @@ -0,0 +1,21 @@ +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + public class BoxShadowAnimator : Animator + { + static ColorAnimator s_colorAnimator = new ColorAnimator(); + static DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue) + { + return new BoxShadow + { + OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), + OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), + Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur), + Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread), + Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color) + }; + } + } +} diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs new file mode 100644 index 0000000000..2b0b17595c --- /dev/null +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -0,0 +1,88 @@ +using System; +using Avalonia.Animation.Animators; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + public struct BoxShadow + { + public double OffsetX { get; set; } + public double OffsetY { get; set; } + public double Blur { get; set; } + public double Spread { get; set; } + public Color Color { get; set; } + + static BoxShadow() + { + Animation.Animation.RegisterAnimator(prop => + typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); + } + + public bool Equals(BoxShadow other) + { + return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); + } + + public override bool Equals(object obj) + { + return obj is BoxShadow other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = OffsetX.GetHashCode(); + hashCode = (hashCode * 397) ^ OffsetY.GetHashCode(); + hashCode = (hashCode * 397) ^ Blur.GetHashCode(); + hashCode = (hashCode * 397) ^ Spread.GetHashCode(); + hashCode = (hashCode * 397) ^ Color.GetHashCode(); + return hashCode; + } + } + + public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + + public static unsafe BoxShadow Parse(string s) + { + if (s == "none") + return default; + + var separatorCount = 0; + var separators = stackalloc char[4]; + for(var c = 0; c 4) + throw new FormatException("Invalid box-shadow format"); + + var tokenizer = new StringTokenizer(s); + var offsetX = tokenizer.ReadDouble(separators[0]); + var offsetY = tokenizer.ReadDouble(separators[1]); + double blur = 0; + double spread = 0; + if (separatorCount > 2) + blur = tokenizer.ReadDouble(separators[2]); + if (separatorCount > 3) + spread = tokenizer.ReadDouble(separators[3]); + var color = Media.Color.Parse(tokenizer.ReadString()); + return new BoxShadow + { + OffsetX = offsetX, + OffsetY = offsetY, + Blur = blur, + Spread = spread, + Color = color + }; + } + + public Rect TransformBounds(in Rect rect) + => rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); + } +} diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index b9a812be80..2264fc327e 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -118,6 +118,50 @@ namespace Avalonia.Media throw new FormatException($"Invalid color string: '{s}'."); } + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(Span s, out Color color) + { + color = default; + if (s == null) + return false; + if (s.Length == 0) + return false; + + if (s[0] == '#') + { + var or = 0u; + + if (s.Length == 7) + { + or = 0xff000000; + } + else if (s.Length != 9) + { + return false; + } + + if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + return false; + color = FromUInt32(parsed| or); + return true; + } + + var knownColor = KnownColors.GetKnownColor(s.ToString()); + + if (knownColor != KnownColor.None) + { + color = knownColor.ToColor(); + return true; + } + + return false; + } + /// /// Returns the string representation of the color. /// diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4045b92c0c..49690adbed 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -125,7 +125,7 @@ namespace Avalonia.Media if (brush != null || PenIsVisible(pen)) { - PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); + PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl, default); } } @@ -141,11 +141,13 @@ namespace Avalonia.Media /// The radius in the Y dimension of the rounded corners. /// This value will be clamped to the range of 0 to Height/2 /// + /// Box shadow effect parameters /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadow boxShadow = default) { if (brush == null && !PenIsVisible(pen)) { @@ -162,7 +164,7 @@ namespace Avalonia.Media radiusY = Math.Min(radiusY, rect.Height / 2); } - PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY); + PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY, boxShadow); } /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index b89039698e..143348a463 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -55,7 +55,8 @@ namespace Avalonia.Platform /// The fill brush. /// The stroke pen. /// The geometry. - void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); + /// The box shadow parameters + void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow); /// /// Draws a rectangle with the specified Brush and Pen. @@ -69,11 +70,13 @@ namespace Avalonia.Platform /// The radius in the Y dimension of the rounded corners. /// This value will be clamped to the range of 0 to Height/2 /// + /// Box shadow effect parameters /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// - void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0); + void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadow boxShadow = default); /// /// Draws text. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index bfa1c08034..c5989459a9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -97,13 +97,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry, boxShadow)) { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + Add(new GeometryNode(Transform, brush, pen, geometry, boxShadow, CreateChildScene(brush))); } else { @@ -149,13 +149,14 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, + BoxShadow boxShadow = default) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY, boxShadow)) { - Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, CreateChildScene(brush))); + Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, boxShadow, CreateChildScene(brush))); } else { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 2e5ca973ec..f93417a0f1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -19,18 +19,20 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The geometry. /// Child scenes for drawing visual brushes. - public GeometryNode( - Matrix transform, + /// + public GeometryNode(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry, + BoxShadow boxShadow, IDictionary childScenes = null) - : base(geometry.GetRenderBounds(pen), transform, null) + : base(boxShadow.TransformBounds(geometry.GetRenderBounds(pen)), transform, null) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; + BoxShadow = boxShadow; ChildScenes = childScenes; } @@ -54,6 +56,8 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } + public BoxShadow BoxShadow { get; } + /// public override IDictionary ChildScenes { get; } @@ -64,24 +68,27 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The geometry of the other draw operation. + /// The box shadow parameters /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry) + public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry, + BoxShadow boxShadow) { return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - Equals(geometry, Geometry); + BoxShadow.Equals(boxShadow, boxShadow) && + Equals(brush, Brush) && + Equals(Pen, pen) && + Equals(geometry, Geometry); } /// public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawGeometry(Brush, Pen, Geometry); + context.DrawGeometry(Brush, Pen, Geometry, BoxShadow); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index e48bad6433..aef7f7f260 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -29,8 +29,9 @@ namespace Avalonia.Rendering.SceneGraph Rect rect, double radiusX, double radiusY, + BoxShadow boxShadow, IDictionary childScenes = null) - : base(rect, transform, pen) + : base(boxShadow.TransformBounds(rect), transform, pen) { Transform = transform; Brush = brush?.ToImmutable(); @@ -39,6 +40,7 @@ namespace Avalonia.Rendering.SceneGraph RadiusX = radiusX; RadiusY = radiusY; ChildScenes = childScenes; + BoxShadow = boxShadow; } /// @@ -70,6 +72,11 @@ namespace Avalonia.Rendering.SceneGraph /// The radius in the Y dimension of the rounded corners. /// public double RadiusY { get; } + + /// + /// The parameters for the box-shadow effect + /// + public BoxShadow BoxShadow { get; } /// public override IDictionary ChildScenes { get; } @@ -88,11 +95,12 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY, BoxShadow boxShadow) { return transform == Transform && Equals(brush, Brush) && Equals(Pen, pen) && + Media.BoxShadow.Equals(BoxShadow, boxShadow) && rect == Rect && Math.Abs(radiusX - RadiusX) < double.Epsilon && Math.Abs(radiusY - RadiusY) < double.Epsilon; @@ -103,7 +111,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY); + context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY, BoxShadow); } /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 9f99ed3cef..33f6e8ec33 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -164,11 +164,21 @@ namespace Avalonia.Skia } /// - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow) { var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; + if(!boxShadow.IsEmpty) + using (var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, false)) + { + Canvas.Save(); + Canvas.ClipPath(impl.EffectivePath, SKClipOperation.Difference); + Canvas.DrawPath(impl.EffectivePath, shadow.Paint); + Canvas.Restore(); + + } + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { @@ -184,12 +194,65 @@ namespace Avalonia.Skia } } + struct BoxShadowFilter : IDisposable + { + public SKPaint Paint; + private SKImageFilter _filter; + private SKImageFilter _dilate; + + public static BoxShadowFilter Create(BoxShadow shadow, double opacity, bool skipDilate) + { + var ac = shadow.Color; + var spread = (int)shadow.Spread; + var dilate = !skipDilate && spread != 0 ? SKImageFilter.CreateDilate(spread, spread) : null; + var filter = SKImageFilter.CreateDropShadow( + (float)shadow.OffsetX, + (float)shadow.OffsetY, + (float)shadow.Blur / 2, + (float)shadow.Blur / 2, + new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)), + SKDropShadowImageFilterShadowMode.DrawShadowOnly, dilate); + var paint = new SKPaint { Color = SKColors.White, ImageFilter = filter }; + return new BoxShadowFilter { Paint = paint, _filter = filter, _dilate = dilate }; + } + + public void Dispose() + { + Paint.Dispose(); + _filter.Dispose(); + _dilate?.Dispose(); + } + } + /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, + BoxShadow boxShadow = default) { var rc = rect.ToSKRect(); var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + if (!boxShadow.IsEmpty) + { + using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true)) + { + var shadowRect = rc; + shadowRect.Inflate((float)boxShadow.Spread, (float)boxShadow.Spread); + Canvas.Save(); + if (isRounded) + { + Canvas.ClipRoundRect(new SKRoundRect(rc, (float)radiusX, (float)radiusY), + SKClipOperation.Difference, true); + Canvas.DrawRoundRect(shadowRect, (float)radiusX, (float)radiusY, shadow.Paint); + } + else + { + Canvas.ClipRect(shadowRect, SKClipOperation.Difference); + Canvas.DrawRect(shadowRect, shadow.Paint); + } + Canvas.Restore(); + } + } + if (brush != null) { using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a2e529395c..78f0f64b5f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -199,7 +199,8 @@ namespace Avalonia.Direct2D1.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, + BoxShadow boxShadow) { if (brush != null) { @@ -228,7 +229,8 @@ namespace Avalonia.Direct2D1.Media } /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, + BoxShadow boxShadow = default) { var rc = rect.ToDirect2D(); var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index fffd36d30a..216527cf8a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -473,7 +473,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var animation = new BehaviorSubject(0.5); context.Verify(x => x.PushOpacity(0.5), Times.Once); - context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once); context.Verify(x => x.PopOpacity(), Times.Once); } } @@ -503,7 +503,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var animation = new BehaviorSubject(0.5); context.Verify(x => x.PushOpacity(0.5), Times.Never); - context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Never); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Never); context.Verify(x => x.PopOpacity(), Times.Never); } } @@ -528,7 +528,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var animation = new BehaviorSubject(0.5); context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once); - context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once); context.Verify(x => x.PopOpacityMask(), Times.Once); } } @@ -653,7 +653,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var context = GetLayerContext(target, border); context.Verify(x => x.PushOpacity(0.5), Times.Never); - context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default), Times.Once); context.Verify(x => x.PopOpacity(), Times.Never); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs index 5fe92ba039..e334fc74cd 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Not_Replace_Identical_DrawOperation() { var node = new VisualNode(new TestRoot(), null); - var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default)); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); @@ -133,7 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Replace_Different_DrawOperation() { var node = new VisualNode(new TestRoot(), null); - var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default)); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); @@ -155,7 +155,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Update_DirtyRects() { var node = new VisualNode(new TestRoot(), null); - var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0); + var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); @@ -206,7 +206,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Trimmed_DrawOperations_Releases_Reference() { var node = new VisualNode(new TestRoot(), null); - var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0, default)); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 6a1f08a384..7787ac0871 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph new Matrix(), Brushes.Black, null, - geometry); + geometry, default); geometryNode.HitTest(new Point()); }