Browse Source

box-shadow support

pull/3871/head
Nikita Tsukanov 7 years ago
parent
commit
20f6f886f9
  1. 19
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 23
      src/Avalonia.Controls/Border.cs
  3. 19
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 10
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 21
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  6. 88
      src/Avalonia.Visuals/Media/BoxShadow.cs
  7. 44
      src/Avalonia.Visuals/Media/Color.cs
  8. 8
      src/Avalonia.Visuals/Media/DrawingContext.cs
  9. 7
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  10. 13
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  11. 23
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  12. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  13. 67
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  14. 6
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  15. 8
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  16. 8
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  17. 2
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

19
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -134,6 +134,23 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Shadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="-15 -15 Red"/>
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="BoxShadow" Value="20 20 0 0 Blue"/>
</KeyFrame>
<KeyFrame Cue="90%">
<Setter Property="BoxShadow" Value="20 20 10 20 Green"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -152,6 +169,8 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Red"/>
<Border Classes="Test Shadow" Background="PaleGreen" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" Background="PaleGreen" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

23
src/Avalonia.Controls/Border.cs

@ -33,6 +33,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadow> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadow>(nameof(BoxShadow));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -44,7 +50,8 @@ namespace Avalonia.Controls
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty);
CornerRadiusProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -83,14 +90,24 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadow BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
/// <param name="context">The drawing context.</param>
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);
}
/// <summary>

19
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadow> BoxShadowProperty =
Border.BoxShadowProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadow BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Gets the control displayed by the presenter.
/// </summary>
@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
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);
}
/// <summary>

10
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);
}
}

21
src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs

@ -0,0 +1,21 @@
using Avalonia.Media;
namespace Avalonia.Animation.Animators
{
public class BoxShadowAnimator : Animator<BoxShadow>
{
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)
};
}
}
}

88
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<BoxShadowAnimator>(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<s.Length; c++)
if (s[c] == ',' || s[c] == ' ')
{
if (separatorCount == 4)
throw new FormatException("Invalid box-shadow format");
separators[separatorCount] = s[c];
separatorCount++;
}
if (separatorCount != 2 && separatorCount > 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);
}
}

44
src/Avalonia.Visuals/Media/Color.cs

@ -118,6 +118,50 @@ namespace Avalonia.Media
throw new FormatException($"Invalid color string: '{s}'.");
}
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns>
public static bool TryParse(Span<char> 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;
}
/// <summary>
/// Returns the string representation of the color.
/// </summary>

8
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
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadow">Box shadow effect parameters</param>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary>

7
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -55,7 +55,8 @@ namespace Avalonia.Platform
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
/// <param name="boxShadow">The box shadow parameters</param>
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow);
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
@ -69,11 +70,13 @@ namespace Avalonia.Platform
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadow">Box shadow effect parameters</param>
/// <remarks>
/// 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.
/// </remarks>
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);
/// <summary>
/// Draws text.

13
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -97,13 +97,13 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow)
{
var next = NextDrawAs<GeometryNode>();
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
}
/// <inheritdoc/>
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<RectangleNode>();
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
{

23
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -19,18 +19,20 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public GeometryNode(
Matrix transform,
/// <param name="boxShadow"></param>
public GeometryNode(Matrix transform,
IBrush brush,
IPen pen,
IGeometryImpl geometry,
BoxShadow boxShadow,
IDictionary<IVisual, Scene> 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
/// </summary>
public IGeometryImpl Geometry { get; }
public BoxShadow BoxShadow { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
@ -64,24 +68,27 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="geometry">The geometry of the other draw operation.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
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);
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry);
context.DrawGeometry(Brush, Pen, Geometry, BoxShadow);
}
/// <inheritdoc/>

14
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -29,8 +29,9 @@ namespace Avalonia.Rendering.SceneGraph
Rect rect,
double radiusX,
double radiusY,
BoxShadow boxShadow,
IDictionary<IVisual, Scene> 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;
}
/// <summary>
@ -70,6 +72,11 @@ namespace Avalonia.Rendering.SceneGraph
/// The radius in the Y dimension of the rounded corners.
/// </summary>
public double RadiusY { get; }
/// <summary>
/// The parameters for the box-shadow effect
/// </summary>
public BoxShadow BoxShadow { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> 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.
/// </remarks>
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);
}
/// <inheritdoc/>

67
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -164,11 +164,21 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
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))

6
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -199,7 +199,8 @@ namespace Avalonia.Direct2D1.Media
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
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
}
/// <inheritdoc />
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;

8
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@ -473,7 +473,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject<double>(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<double>(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<double>(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);
}
}

8
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);

2
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());
}

Loading…
Cancel
Save