From 20f6f886f9a7b0a7b2fbff029a5e75e3486986bb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 03:08:05 +0300 Subject: [PATCH 1/7] 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()); } From 83b9e78f63e4275f1e6a36d728aee7f7634150ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 03:25:46 +0300 Subject: [PATCH 2/7] Fixed demo animation --- samples/RenderDemo/Pages/AnimationsPage.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 5f8b408622..6633c6cd8b 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -145,7 +145,7 @@ - + From d0b041095d60413f57e7bae14a46289011e3c1fa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 15:41:21 +0300 Subject: [PATCH 3/7] Inset box-shadow --- samples/RenderDemo/Pages/AnimationsPage.xaml | 27 ++++ src/Avalonia.Controls/Border.cs | 2 - .../Presenters/ContentPresenter.cs | 2 - .../Utils/BorderRenderHelper.cs | 50 +++++-- .../Animation/Animators/BoxShadowAnimator.cs | 4 +- src/Avalonia.Visuals/Media/BoxShadow.cs | 95 ++++++++---- src/Avalonia.Visuals/Media/DrawingContext.cs | 4 +- .../Platform/IDrawingContextImpl.cs | 11 +- .../Platform/IPlatformRenderInterface.cs | 2 + src/Avalonia.Visuals/Rect.cs | 10 ++ .../SceneGraph/DeferredDrawingContextImpl.cs | 12 +- .../Rendering/SceneGraph/GeometryNode.cs | 13 +- .../Rendering/SceneGraph/RectangleNode.cs | 40 ++---- src/Avalonia.Visuals/RoundedRect.cs | 136 ++++++++++++++++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 122 +++++++++++----- .../Avalonia.Skia/PlatformRenderInterface.cs | 2 + src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 5 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 2 + .../Media/DrawingContextImpl.cs | 13 +- .../NullRenderingPlatform.cs | 2 + .../MockPlatformRenderInterface.cs | 2 + .../Media/BoxShadowTests.cs | 45 ++++++ .../Rendering/DeferredRendererTests.cs | 8 +- .../DeferredDrawingContextImplTests.cs | 8 +- .../VisualTree/MockRenderInterface.cs | 2 + 25 files changed, 475 insertions(+), 144 deletions(-) create mode 100644 src/Avalonia.Visuals/RoundedRect.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 6633c6cd8b..bc20b82053 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -151,6 +151,31 @@ + @@ -171,6 +196,8 @@ + + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index d60a41c543..2471aba839 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -127,8 +127,6 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { - _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius); - return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness); } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 6f3aa90d09..caefc48055 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -336,8 +336,6 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { - _borderRenderer.Update(finalSize, BorderThickness, CornerRadius); - return ArrangeOverrideImpl(finalSize, new Vector()); } diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 63f0dc1015..4763773106 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -7,12 +7,24 @@ namespace Avalonia.Controls.Utils internal class BorderRenderHelper { private bool _useComplexRendering; + private bool? _backendSupportsIndividualCorners; private StreamGeometry _backgroundGeometryCache; private StreamGeometry _borderGeometryCache; + private Size _size; + private Thickness _borderThickness; + private CornerRadius _cornerRadius; + private bool _initialized; - public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) + void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) { - if (borderThickness.IsUniform && cornerRadius.IsUniform) + _backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService() + .SupportsIndividualRoundRects; + _size = finalSize; + _borderThickness = borderThickness; + _cornerRadius = cornerRadius; + _initialized = true; + + if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true)) { _backgroundGeometryCache = null; _borderGeometryCache = null; @@ -68,19 +80,28 @@ namespace Avalonia.Controls.Utils } } - public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, - IBrush borderBrush, BoxShadow boxShadow) + public void Render(DrawingContext context, + Size finalSize, Thickness borderThickness, CornerRadius cornerRadius, + IBrush background, IBrush borderBrush, BoxShadow boxShadow) + { + if (_size != finalSize + || _borderThickness != borderThickness + || _cornerRadius != cornerRadius + || !_initialized) + Update(finalSize, borderThickness, cornerRadius); + RenderCore(context, background, borderBrush, boxShadow); + } + + void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadow boxShadow) { if (_useComplexRendering) { var backgroundGeometry = _backgroundGeometryCache; if (backgroundGeometry != null) { - // 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); + context.DrawGeometry(background, null, backgroundGeometry); } - + var borderGeometry = _borderGeometryCache; if (borderGeometry != null) { @@ -89,9 +110,7 @@ namespace Avalonia.Controls.Utils } else { - var borderThickness = borders.Top; - var top = borderThickness * 0.5; - + var borderThickness = _borderThickness.Top; IPen pen = null; if (borderThickness > 0) @@ -99,9 +118,14 @@ namespace Avalonia.Controls.Utils pen = new Pen(borderBrush, borderThickness); } - var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness); + var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight, + _cornerRadius.BottomRight, _cornerRadius.BottomLeft); + if (Math.Abs(borderThickness) > double.Epsilon) + { + rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5); + } - context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft, boxShadow); + context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadow); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs index fced815b85..ff76902425 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs @@ -6,6 +6,7 @@ namespace Avalonia.Animation.Animators { static ColorAnimator s_colorAnimator = new ColorAnimator(); static DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + static BoolAnimator s_boolAnimator = new BoolAnimator(); public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue) { return new BoxShadow @@ -14,7 +15,8 @@ namespace Avalonia.Animation.Animators 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) + Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color), + IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset) }; } } diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs index 2b0b17595c..596cb71581 100644 --- a/src/Avalonia.Visuals/Media/BoxShadow.cs +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -11,6 +12,7 @@ namespace Avalonia.Media public double Blur { get; set; } public double Spread { get; set; } public Color Color { get; set; } + public bool IsInset { get; set; } static BoxShadow() { @@ -43,37 +45,82 @@ namespace Avalonia.Media public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + private readonly static char[] s_Separator = new char[] { ' ', '\t' }; + + struct ArrayReader + { + private int _index; + private string[] _arr; + + public ArrayReader(string[] arr) + { + _arr = arr; + _index = 0; + } + + public bool TryReadString(out string s) + { + s = null; + if (_index >= _arr.Length) + return false; + s = _arr[_index]; + _index++; + return true; + } + + public string ReadString() + { + if(!TryReadString(out var rv)) + throw new FormatException(); + return rv; + } + } public static unsafe BoxShadow Parse(string s) { + if(s == null) + throw new ArgumentNullException(); + if (s.Length == 0) + throw new FormatException(); + if (s[0] == ' ' || s[s.Length - 1] == ' ') + s = s.Trim(); + if (s == "none") return default; + + var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries); + if (p.Length < 3 || p.Length > 6) + throw new FormatException(); - 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]); + bool inset = false; + + var tokenizer = new ArrayReader(p); + + string firstToken = tokenizer.ReadString(); + if (firstToken == "inset") + { + inset = true; + firstToken = tokenizer.ReadString(); + } + + var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture); + var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture); 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()); + + + tokenizer.TryReadString(out var token3); + tokenizer.TryReadString(out var token4); + tokenizer.TryReadString(out var token5); + + if (token4 != null) + blur = double.Parse(token3, CultureInfo.InvariantCulture); + if (token5 != null) + spread = double.Parse(token4, CultureInfo.InvariantCulture); + + var color = Color.Parse(token5 ?? token4 ?? token3); return new BoxShadow { + IsInset = inset, OffsetX = offsetX, OffsetY = offsetY, Blur = blur, @@ -82,7 +129,7 @@ namespace Avalonia.Media }; } - public Rect TransformBounds(in Rect rect) - => rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); + public Rect TransformBounds(in Rect rect) + => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 49690adbed..4df26c470d 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, default); + PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); } } @@ -164,7 +164,7 @@ namespace Avalonia.Media radiusY = Math.Min(radiusY, rect.Height / 2); } - PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY, boxShadow); + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow); } /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 143348a463..870698316c 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -55,8 +55,7 @@ namespace Avalonia.Platform /// The fill brush. /// The stroke pen. /// The geometry. - /// The box shadow parameters - void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow); + void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// /// Draws a rectangle with the specified Brush and Pen. @@ -64,18 +63,12 @@ namespace Avalonia.Platform /// The brush used to fill the rectangle, or null for no fill. /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. - /// The radius in the X dimension of the rounded corners. - /// This value will be clamped to the range of 0 to Width/2 - /// - /// 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, RoundedRect rect, BoxShadow boxShadow = default); /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index bd569fe841..a0102a0f33 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -116,5 +116,7 @@ namespace Avalonia.Platform /// The glyph run's width. /// IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width); + + bool SupportsIndividualRoundRects { get; } } } diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index a7aa16684e..b0c4cb62eb 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -132,6 +132,16 @@ namespace Avalonia /// Gets the bottom position of the rectangle. /// public double Bottom => _y + _height; + + /// + /// Gets the left position. + /// + public double Left => _x; + + /// + /// Gets the top position. + /// + public double Top => _y; /// /// Gets the top left point of the rectangle. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index c5989459a9..31707ab5cc 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, BoxShadow boxShadow) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry, boxShadow)) + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { - Add(new GeometryNode(Transform, brush, pen, geometry, boxShadow, CreateChildScene(brush))); + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); } else { @@ -149,14 +149,14 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadow boxShadow = default) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY, boxShadow)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadow)) { - Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, boxShadow, CreateChildScene(brush))); + Add(new RectangleNode(Transform, brush, pen, rect, boxShadow, CreateChildScene(brush))); } else { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index f93417a0f1..cb7498f7b7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -19,20 +19,17 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The geometry. /// Child scenes for drawing visual brushes. - /// public GeometryNode(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry, - BoxShadow boxShadow, IDictionary childScenes = null) - : base(boxShadow.TransformBounds(geometry.GetRenderBounds(pen)), transform, null) + : base(geometry.GetRenderBounds(pen), transform, null) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - BoxShadow = boxShadow; ChildScenes = childScenes; } @@ -56,8 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - public BoxShadow BoxShadow { get; } - /// public override IDictionary ChildScenes { get; } @@ -74,11 +69,9 @@ 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, IGeometryImpl geometry, - BoxShadow boxShadow) + public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry) { return transform == Transform && - BoxShadow.Equals(boxShadow, boxShadow) && Equals(brush, Brush) && Equals(Pen, pen) && Equals(geometry, Geometry); @@ -88,7 +81,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawGeometry(Brush, Pen, Geometry, BoxShadow); + context.DrawGeometry(Brush, Pen, Geometry); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index aef7f7f260..adf3f20c1b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,26 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The radius in the Y dimension of the rounded corners. - /// The radius in the X dimension of the rounded corners. + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, IBrush brush, IPen pen, - Rect rect, - double radiusX, - double radiusY, + RoundedRect rect, BoxShadow boxShadow, IDictionary childScenes = null) - : base(boxShadow.TransformBounds(rect), transform, pen) + : base(boxShadow.TransformBounds(rect.Rect), transform, pen) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - RadiusX = radiusX; - RadiusY = radiusY; ChildScenes = childScenes; BoxShadow = boxShadow; } @@ -61,17 +56,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the rectangle to draw. /// - public Rect Rect { get; } - - /// - /// The radius in the X dimension of the rounded corners. - /// - public double RadiusX { get; } - - /// - /// The radius in the Y dimension of the rounded corners. - /// - public double RadiusY { get; } + public RoundedRect Rect { get; } /// /// The parameters for the box-shadow effect @@ -88,22 +73,19 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The rectangle of the other draw operation. - /// - /// + /// The box shadow parameters of the other draw operation /// 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, Rect rect, double radiusX, double radiusY, BoxShadow boxShadow) + public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, 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; + rect.Equals(Rect); } /// @@ -111,7 +93,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY, BoxShadow); + context.DrawRectangle(Brush, Pen, Rect, BoxShadow); } /// @@ -124,13 +106,13 @@ namespace Avalonia.Rendering.SceneGraph if (Brush != null) { - var rect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); return rect.Contains(p); } else { - var borderRect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Deflate((Pen?.Thickness / 2) ?? 0); + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); return borderRect.Contains(p) && !emptyRect.Contains(p); } } diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Visuals/RoundedRect.cs new file mode 100644 index 0000000000..ad860240f2 --- /dev/null +++ b/src/Avalonia.Visuals/RoundedRect.cs @@ -0,0 +1,136 @@ +using System; + +namespace Avalonia +{ + public struct RoundedRect + { + public bool Equals(RoundedRect other) + { + return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight); + } + + public override bool Equals(object obj) + { + return obj is RoundedRect other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Rect.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode(); + return hashCode; + } + } + + public Rect Rect { get; } + public Vector RadiiTopLeft { get; } + public Vector RadiiTopRight { get; } + public Vector RadiiBottomLeft { get; } + public Vector RadiiBottomRight { get; } + + public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft) + { + Rect = rect; + RadiiTopLeft = radiiTopLeft; + RadiiTopRight = radiiTopRight; + RadiiBottomRight = radiiBottomRight; + RadiiBottomLeft = radiiBottomLeft; + } + + public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight, + double radiusBottomLeft) + : this(rect, + new Vector(radiusTopLeft, radiusTopLeft), + new Vector(radiusTopRight, radiusTopRight), + new Vector(radiusBottomRight, radiusBottomRight), + new Vector(radiusBottomLeft, radiusBottomLeft) + ) + { + + } + + public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii) + { + + } + + public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY)) + { + + } + + public RoundedRect(Rect rect, double radius) : this(rect, radius, radius) + { + + } + + public RoundedRect(Rect rect) : this(rect, 0) + { + + } + + public static implicit operator RoundedRect(Rect r) => new RoundedRect(r); + + public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default || + RadiiBottomLeft != default; + + public bool IsUniform => + RadiiTopLeft.Equals(RadiiTopRight) && + RadiiTopLeft.Equals(RadiiBottomRight) && + RadiiTopLeft.Equals(RadiiBottomLeft); + + public RoundedRect Inflate(double dx, double dy) + { + return Deflate(-dx, -dy); + } + + public unsafe RoundedRect Deflate(double dx, double dy) + { + if (!IsRounded) + return new RoundedRect(Rect.Deflate(new Thickness(dx, dy))); + + // Ported from SKRRect + var left = Rect.X + dx; + var top = Rect.Y + dy; + var right = left + Rect.Width - dx * 2; + var bottom = top + Rect.Height - dy * 2; + var radii = stackalloc Vector[4]; + radii[0] = RadiiTopLeft; + radii[1] = RadiiTopRight; + radii[2] = RadiiBottomRight; + radii[3] = RadiiBottomLeft; + + bool degenerate = false; + if (right <= left) { + degenerate = true; + left = right = (left + right)*0.5; + } + if (bottom <= top) { + degenerate = true; + top = bottom = (top + bottom) * 0.5; + } + if (degenerate) + { + return new RoundedRect(new Rect(left, top, right - left, bottom - top)); + } + + for (var c = 0; c < 4; c++) + { + var rx = Math.Max(0, radii[c].X - dx); + var ry = Math.Max(0, radii[c].Y - dy); + if (rx == 0 || ry == 0) + radii[c] = default; + else + radii[c] = new Vector(rx, ry); + } + + return new RoundedRect(new Rect(left, top, right - left, bottom - top), + radii[0], radii[1], radii[2], radii[3]); + } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 33f6e8ec33..3b80c7f23c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -164,21 +164,11 @@ namespace Avalonia.Skia } /// - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { 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)) { @@ -198,55 +188,99 @@ namespace Avalonia.Skia { public SKPaint Paint; private SKImageFilter _filter; - private SKImageFilter _dilate; + public SKClipOperation ClipOperation; 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; + if (shadow.IsInset) + spread = -spread; + 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); + SKDropShadowImageFilterShadowMode.DrawShadowOnly, null); + var paint = new SKPaint { Color = SKColors.White, ImageFilter = filter }; - return new BoxShadowFilter { Paint = paint, _filter = filter, _dilate = dilate }; + + return new BoxShadowFilter + { + Paint = paint, _filter = filter, + ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference + }; } public void Dispose() { Paint.Dispose(); _filter.Dispose(); - _dilate?.Dispose(); } } - - /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, - BoxShadow boxShadow = default) + + SKRect AreaCastingShadowInHole( + SKRect hole_rect, + float shadow_blur, + float shadow_spread, + float offsetX, float offsetY) { - var rc = rect.ToSKRect(); - var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + // Adapted from Chromium + var bounds = hole_rect; + + bounds.Inflate(shadow_blur, shadow_blur); + + if (shadow_spread < 0) + bounds.Inflate(-shadow_spread, -shadow_spread); + + var offset_bounds = bounds; + offset_bounds.Offset(-offsetX, -offsetY); + bounds.Union(offset_bounds); + return bounds; + } + - if (!boxShadow.IsEmpty) + /// + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadow boxShadow = default) + { + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded || (!boxShadow.IsEmpty && boxShadow.IsInset); + using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + if (needRoundRect) + skRoundRect.SetRectRadii(rc, + new[] + { + rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + }); + + if (!boxShadow.IsEmpty && !boxShadow.IsInset) { using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true)) { - var shadowRect = rc; - shadowRect.Inflate((float)boxShadow.Spread, (float)boxShadow.Spread); + var spread = (float)boxShadow.Spread; + if (boxShadow.IsInset) + spread = -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); + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + Canvas.DrawRoundRect(shadowRect, shadow.Paint); } else { - Canvas.ClipRect(shadowRect, SKClipOperation.Difference); + var shadowRect = rc; + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRect(shadowRect, shadow.ClipOperation); Canvas.DrawRect(shadowRect, shadow.Paint); } Canvas.Restore(); @@ -255,11 +289,11 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) { if (isRounded) { - Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + Canvas.DrawRoundRect(skRoundRect, paint.Paint); } else { @@ -269,13 +303,35 @@ namespace Avalonia.Skia } } + if (!boxShadow.IsEmpty && boxShadow.IsInset) + { + using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true)) + { + var spread = (float)boxShadow.Spread; + var offsetX = (float)boxShadow.OffsetX; + var offsetY = (float)boxShadow.OffsetY; + var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); + + Canvas.Save(); + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Deflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + using (var outerRRect = new SKRoundRect(outerRect)) + Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); + + Canvas.Restore(); + } + } + if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) { if (isRounded) { - Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + Canvas.DrawRoundRect(skRoundRect, paint.Paint); } else { diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 46872f903e..e55ae7e7d1 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -247,5 +247,7 @@ namespace Avalonia.Skia return new GlyphRunImpl(textBlob); } + + public bool SupportsIndividualRoundRects => true; } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 1dd2310475..459486f784 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -11,6 +11,11 @@ namespace Avalonia.Skia { return new SKPoint((float)p.X, (float)p.Y); } + + public static SKPoint ToSKPoint(this Vector p) + { + return new SKPoint((float)p.X, (float)p.Y); + } public static SKRect ToSKRect(this Rect r) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index b8bbed24f8..96bd96341c 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -240,5 +240,7 @@ namespace Avalonia.Direct2D1 return new GlyphRunImpl(run); } + + public bool SupportsIndividualRoundRects => false; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 78f0f64b5f..80278597dd 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -199,8 +199,7 @@ namespace Avalonia.Direct2D1.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, - BoxShadow boxShadow) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { if (brush != null) { @@ -229,10 +228,14 @@ namespace Avalonia.Direct2D1.Media } /// - public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0D, double radiusY = 0D, - BoxShadow boxShadow = default) + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadow boxShadow = default) { - var rc = rect.ToDirect2D(); + var rc = rrect.Rect.ToDirect2D(); + var rect = rrect.Rect; + var radiusX = Math.Max(rrect.RadiiTopLeft.X, + Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X))); + var radiusY = Math.Max(rrect.RadiiTopLeft.Y, + Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y))); var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; if (brush != null) diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 1f983069c2..5e29893946 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -76,5 +76,7 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } + + public bool SupportsIndividualRoundRects => true; } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 23b6a00cc8..afdf95430b 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -84,5 +84,7 @@ namespace Avalonia.UnitTests width = 0; return Mock.Of(); } + + public bool SupportsIndividualRoundRects { get; set; } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs new file mode 100644 index 0000000000..e3b703baf2 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs @@ -0,0 +1,45 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class BoxShadowTests + { + [Fact] + public void BoxShadow_Should_Parse() + { + foreach (var extraSpaces in new[] { false, true }) + foreach (var inset in new[] { false, true }) + for (var componentCount = 2; componentCount < 5; componentCount++) + { + var s = (inset ? "inset " : "") + "10 20"; + double blur = 0; + double spread = 0; + if (componentCount > 2) + { + s += " 30"; + blur = 30; + } + + if (componentCount > 3) + { + s += " 40"; + spread = 40; + } + + s += " red"; + + if (extraSpaces) + s = " " + s.Replace(" ", " ") + " "; + + var parsed = BoxShadow.Parse(s); + Assert.Equal(inset, parsed.IsInset); + Assert.Equal(10, parsed.OffsetX); + Assert.Equal(20, parsed.OffsetY); + Assert.Equal(blur, parsed.Blur); + Assert.Equal(spread, parsed.Spread); + Assert.Equal(Colors.Red, parsed.Color); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 216527cf8a..767111b89b 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, default), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default), Times.Never); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default), Times.Once); + context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 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 e334fc74cd..27aa0c55b5 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, default)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default); + var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 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, default)); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default)); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 28304b674b..019eefe1a6 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -56,6 +56,8 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public bool SupportsIndividualRoundRects { get; set; } + public IFontManagerImpl CreateFontManager() { return new MockFontManagerImpl(); From e9a85b71e8cef8f2f198bb2fe8bacccfc115072b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 17:58:21 +0300 Subject: [PATCH 4/7] Update src/Skia/Avalonia.Skia/DrawingContextImpl.cs Co-authored-by: Benedikt Stebner --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 3b80c7f23c..af6ae7fdfb 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -205,7 +205,7 @@ namespace Avalonia.Skia new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)), SKDropShadowImageFilterShadowMode.DrawShadowOnly, null); - var paint = new SKPaint { Color = SKColors.White, ImageFilter = filter }; + var paint = new SKPaint { IsAntialias = true, Color = SKColors.White, ImageFilter = filter }; return new BoxShadowFilter { From edd5510ce2b948bcc755de4019e6f6c791e0e2ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 May 2020 18:05:51 +0300 Subject: [PATCH 5/7] Reuse SKPaint instance used for box shadows --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index af6ae7fdfb..166a7a72f2 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -33,6 +33,7 @@ namespace Avalonia.Skia private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); + private readonly SKPaint _boxShadowPaint = new SKPaint(); /// /// Context create info. @@ -190,7 +191,7 @@ namespace Avalonia.Skia private SKImageFilter _filter; public SKClipOperation ClipOperation; - public static BoxShadowFilter Create(BoxShadow shadow, double opacity, bool skipDilate) + public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity, bool skipDilate) { var ac = shadow.Color; var spread = (int)shadow.Spread; @@ -205,7 +206,10 @@ namespace Avalonia.Skia new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)), SKDropShadowImageFilterShadowMode.DrawShadowOnly, null); - var paint = new SKPaint { IsAntialias = true, Color = SKColors.White, ImageFilter = filter }; + paint.Reset(); + paint.IsAntialias = true; + paint.Color= SKColors.White; + paint.ImageFilter = filter; return new BoxShadowFilter { @@ -216,7 +220,8 @@ namespace Avalonia.Skia public void Dispose() { - Paint.Dispose(); + Paint.Reset(); + Paint = null; _filter.Dispose(); } } @@ -259,7 +264,7 @@ namespace Avalonia.Skia if (!boxShadow.IsEmpty && !boxShadow.IsInset) { - using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true)) + using(var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity, true)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -305,7 +310,7 @@ namespace Avalonia.Skia if (!boxShadow.IsEmpty && boxShadow.IsInset) { - using(var shadow = BoxShadowFilter.Create(boxShadow, _currentOpacity, true)) + using(var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity, true)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; From d3b8b02fd1ce801f9a5ee3974b57a1dc7a201da8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 3 May 2020 13:28:14 +0300 Subject: [PATCH 6/7] ROS --- src/Avalonia.Visuals/Media/Color.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 2264fc327e..2e06d2578f 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -124,7 +124,7 @@ namespace Avalonia.Media /// The color string. /// The parsed color /// The status of the operation. - public static bool TryParse(Span s, out Color color) + public static bool TryParse(ReadOnlySpan s, out Color color) { color = default; if (s == null) From bea8b676a2c7093a143343a8b18c5ede7fa7bf6f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 3 May 2020 13:29:57 +0300 Subject: [PATCH 7/7] Support for multiple box-shadows --- samples/RenderDemo/Pages/AnimationsPage.xaml | 34 +---- src/Avalonia.Controls/Border.cs | 6 +- .../Presenters/ContentPresenter.cs | 4 +- .../Utils/BorderRenderHelper.cs | 8 +- .../Animation/Animators/BoxShadowsAnimator.cs | 40 +++++ src/Avalonia.Visuals/Media/BoxShadow.cs | 10 +- src/Avalonia.Visuals/Media/BoxShadows.cs | 137 ++++++++++++++++++ .../Platform/IDrawingContextImpl.cs | 4 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 6 +- .../Rendering/SceneGraph/RectangleNode.cs | 14 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 97 +++++++------ .../Media/DrawingContextImpl.cs | 2 +- 12 files changed, 263 insertions(+), 99 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs create mode 100644 src/Avalonia.Visuals/Media/BoxShadows.cs diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index bc20b82053..12fb31ea59 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -135,23 +135,6 @@ - @@ -194,10 +178,8 @@ - - - - + + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 2471aba839..b355310244 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -36,8 +36,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty BoxShadowProperty = - AvaloniaProperty.Register(nameof(BoxShadow)); + public static readonly StyledProperty BoxShadowProperty = + AvaloniaProperty.Register(nameof(BoxShadow)); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); @@ -94,7 +94,7 @@ namespace Avalonia.Controls /// /// Gets or sets the box shadow effect parameters /// - public BoxShadow BoxShadow + public BoxShadows BoxShadow { get => GetValue(BoxShadowProperty); set => SetValue(BoxShadowProperty, value); diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index caefc48055..50aa8a9e71 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -43,7 +43,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty BoxShadowProperty = + public static readonly StyledProperty BoxShadowProperty = Border.BoxShadowProperty.AddOwner(); /// @@ -140,7 +140,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the box shadow effect parameters /// - public BoxShadow BoxShadow + public BoxShadows BoxShadow { get => GetValue(BoxShadowProperty); set => SetValue(BoxShadowProperty, value); diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 4763773106..cd0735d46f 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -82,17 +82,17 @@ namespace Avalonia.Controls.Utils public void Render(DrawingContext context, Size finalSize, Thickness borderThickness, CornerRadius cornerRadius, - IBrush background, IBrush borderBrush, BoxShadow boxShadow) + IBrush background, IBrush borderBrush, BoxShadows boxShadows) { if (_size != finalSize || _borderThickness != borderThickness || _cornerRadius != cornerRadius || !_initialized) Update(finalSize, borderThickness, cornerRadius); - RenderCore(context, background, borderBrush, boxShadow); + RenderCore(context, background, borderBrush, boxShadows); } - void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadow boxShadow) + void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows) { if (_useComplexRendering) { @@ -125,7 +125,7 @@ namespace Avalonia.Controls.Utils rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5); } - context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadow); + context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs new file mode 100644 index 0000000000..c6f96a2d0e --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs @@ -0,0 +1,40 @@ +using Avalonia.Media; + +namespace Avalonia.Animation.Animators +{ + public class BoxShadowsAnimator : Animator + { + private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator(); + public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue) + { + int cnt = progress >= 1d ? newValue.Count : oldValue.Count; + if (cnt == 0) + return new BoxShadows(); + + BoxShadow first; + if (oldValue.Count > 0 && newValue.Count > 0) + first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]); + else if (oldValue.Count > 0) + first = oldValue[0]; + else + first = newValue[0]; + + if (cnt == 1) + return new BoxShadows(first); + + var rest = new BoxShadow[cnt - 1]; + for (var c = 0; c < rest.Length; c++) + { + var idx = c + 1; + if (oldValue.Count > idx && newValue.Count > idx) + rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]); + else if (oldValue.Count > idx) + rest[c] = oldValue[idx]; + else + rest[c] = newValue[idx]; + } + + return new BoxShadows(first, rest); + } + } +} diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs index 596cb71581..69395fd3b8 100644 --- a/src/Avalonia.Visuals/Media/BoxShadow.cs +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -20,7 +20,7 @@ namespace Avalonia.Media typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); } - public bool Equals(BoxShadow other) + public bool Equals(in BoxShadow other) { return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); } @@ -81,13 +81,11 @@ namespace Avalonia.Media throw new ArgumentNullException(); if (s.Length == 0) throw new FormatException(); - if (s[0] == ' ' || s[s.Length - 1] == ' ') - s = s.Trim(); - - if (s == "none") - return default; var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries); + if (p.Length == 1 && p[0] == "none") + return default; + if (p.Length < 3 || p.Length > 6) throw new FormatException(); diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs new file mode 100644 index 0000000000..fd187f6409 --- /dev/null +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.Animation.Animators; + +namespace Avalonia.Media +{ + public struct BoxShadows + { + private readonly BoxShadow _first; + private readonly BoxShadow[] _list; + public int Count { get; } + + static BoxShadows() + { + Animation.Animation.RegisterAnimator(prop => + typeof(BoxShadows).IsAssignableFrom(prop.PropertyType)); + } + + public BoxShadows(BoxShadow shadow) + { + _first = shadow; + _list = null; + Count = 1; + } + + public BoxShadows(BoxShadow first, BoxShadow[] rest) + { + _first = first; + _list = rest; + Count = 1 + (rest?.Length ?? 0); + } + + public BoxShadow this[int c] + { + get + { + if (c< 0 || c >= Count) + throw new IndexOutOfRangeException(); + if (c == 0) + return _first; + return _list[c - 1]; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public struct BoxShadowsEnumerator + { + private int _index; + private BoxShadows _shadows; + + public BoxShadowsEnumerator(BoxShadows shadows) + { + _shadows = shadows; + _index = -1; + } + + public BoxShadow Current => _shadows[_index]; + + public bool MoveNext() + { + _index++; + return _index < _shadows.Count; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this); + + private static readonly char[] s_Separators = new[] { ',' }; + public static BoxShadows Parse(string s) + { + var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries); + if (sp.Length == 0 + || (sp.Length == 1 && + (string.IsNullOrWhiteSpace(sp[0]) + || sp[0] == "none"))) + return new BoxShadows(); + + var first = BoxShadow.Parse(sp[0]); + if (sp.Length == 1) + return new BoxShadows(first); + + var rest = new BoxShadow[sp.Length - 1]; + for (var c = 0; c < rest.Length; c++) + rest[c] = BoxShadow.Parse(sp[c + 1]); + return new BoxShadows(first, rest); + } + + public Rect TransformBounds(in Rect rect) + { + var final = rect; + foreach (var shadow in this) + final = final.Union(shadow.TransformBounds(rect)); + return final; + } + + public bool HasInsetShadows + { + get + { + foreach(var boxShadow in this) + if (!boxShadow.IsEmpty && boxShadow.IsInset) + return true; + return false; + } + } + + public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow); + + public bool Equals(BoxShadows other) + { + if (other.Count != Count) + return false; + for(var c=0; cThe brush used to fill the rectangle, or null for no fill. /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. - /// Box shadow effect parameters + /// 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, RoundedRect rect, - BoxShadow boxShadow = default); + BoxShadows boxShadow = default); /// /// Draws text. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 31707ab5cc..b8658a7a26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -150,13 +150,13 @@ namespace Avalonia.Rendering.SceneGraph /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, - BoxShadow boxShadow = default) + BoxShadows boxShadows = default) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadow)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadow, CreateChildScene(brush))); + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); } else { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index adf3f20c1b..633b1fc5f3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -26,16 +26,16 @@ namespace Avalonia.Rendering.SceneGraph IBrush brush, IPen pen, RoundedRect rect, - BoxShadow boxShadow, + BoxShadows boxShadows, IDictionary childScenes = null) - : base(boxShadow.TransformBounds(rect.Rect), transform, pen) + : base(boxShadows.TransformBounds(rect.Rect), transform, pen) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; ChildScenes = childScenes; - BoxShadow = boxShadow; + BoxShadows = boxShadows; } /// @@ -61,7 +61,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// The parameters for the box-shadow effect /// - public BoxShadow BoxShadow { get; } + public BoxShadows BoxShadows { get; } /// public override IDictionary ChildScenes { get; } @@ -79,12 +79,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, RoundedRect rect, BoxShadow boxShadow) + public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows) { return transform == Transform && Equals(brush, Brush) && Equals(Pen, pen) && - Media.BoxShadow.Equals(BoxShadow, boxShadow) && + Media.BoxShadows.Equals(BoxShadows, boxShadows) && rect.Equals(Rect); } @@ -93,7 +93,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - context.DrawRectangle(Brush, Pen, Rect, BoxShadow); + context.DrawRectangle(Brush, Pen, Rect, BoxShadows); } /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 166a7a72f2..570ed1ac65 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -191,7 +191,7 @@ namespace Avalonia.Skia private SKImageFilter _filter; public SKClipOperation ClipOperation; - public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity, bool skipDilate) + public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity) { var ac = shadow.Color; var spread = (int)shadow.Spread; @@ -248,11 +248,11 @@ namespace Avalonia.Skia /// - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadow boxShadow = default) + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) { var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; - var needRoundRect = rect.IsRounded || (!boxShadow.IsEmpty && boxShadow.IsInset); + var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); using var skRoundRect = needRoundRect ? new SKRoundRect() : null; if (needRoundRect) skRoundRect.SetRectRadii(rc, @@ -262,36 +262,40 @@ namespace Avalonia.Skia rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), }); - if (!boxShadow.IsEmpty && !boxShadow.IsInset) + foreach (var boxShadow in boxShadows) { - using(var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity, true)) + if (!boxShadow.IsEmpty && !boxShadow.IsInset) { - var spread = (float)boxShadow.Spread; - if (boxShadow.IsInset) - spread = -spread; - - Canvas.Save(); - if (isRounded) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { - using var shadowRect = new SKRoundRect(skRoundRect); - if (spread != 0) - shadowRect.Inflate(spread, spread); - Canvas.ClipRoundRect(skRoundRect, - shadow.ClipOperation, true); - Canvas.DrawRoundRect(shadowRect, shadow.Paint); + var spread = (float)boxShadow.Spread; + if (boxShadow.IsInset) + spread = -spread; + + Canvas.Save(); + if (isRounded) + { + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + Canvas.DrawRoundRect(shadowRect, shadow.Paint); + } + else + { + var shadowRect = rc; + if (spread != 0) + shadowRect.Inflate(spread, spread); + Canvas.ClipRect(shadowRect, shadow.ClipOperation); + Canvas.DrawRect(shadowRect, shadow.Paint); + } + + Canvas.Restore(); } - else - { - var shadowRect = rc; - if (spread != 0) - shadowRect.Inflate(spread, spread); - Canvas.ClipRect(shadowRect, shadow.ClipOperation); - Canvas.DrawRect(shadowRect, shadow.Paint); - } - Canvas.Restore(); } } - + if (brush != null) { using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) @@ -308,28 +312,31 @@ namespace Avalonia.Skia } } - if (!boxShadow.IsEmpty && boxShadow.IsInset) + foreach (var boxShadow in boxShadows) { - using(var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity, true)) + if (!boxShadow.IsEmpty && boxShadow.IsInset) { - var spread = (float)boxShadow.Spread; - var offsetX = (float)boxShadow.OffsetX; - var offsetY = (float)boxShadow.OffsetY; - var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); - - Canvas.Save(); - using var shadowRect = new SKRoundRect(skRoundRect); - if (spread != 0) - shadowRect.Deflate(spread, spread); - Canvas.ClipRoundRect(skRoundRect, - shadow.ClipOperation, true); - using (var outerRRect = new SKRoundRect(outerRect)) - Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); - - Canvas.Restore(); + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + { + var spread = (float)boxShadow.Spread; + var offsetX = (float)boxShadow.OffsetX; + var offsetY = (float)boxShadow.OffsetY; + var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); + + Canvas.Save(); + using var shadowRect = new SKRoundRect(skRoundRect); + if (spread != 0) + shadowRect.Deflate(spread, spread); + Canvas.ClipRoundRect(skRoundRect, + shadow.ClipOperation, true); + using (var outerRRect = new SKRoundRect(outerRect)) + Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); + + Canvas.Restore(); + } } } - + if (pen?.Brush != null) { using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 80278597dd..bbb45cf64c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -228,7 +228,7 @@ namespace Avalonia.Direct2D1.Media } /// - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadow boxShadow = default) + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadows boxShadow = default) { var rc = rrect.Rect.ToDirect2D(); var rect = rrect.Rect;