Browse Source

Inset box-shadow

pull/3871/head
Nikita Tsukanov 6 years ago
parent
commit
d0b041095d
  1. 27
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 2
      src/Avalonia.Controls/Border.cs
  3. 2
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 50
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 4
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  6. 95
      src/Avalonia.Visuals/Media/BoxShadow.cs
  7. 4
      src/Avalonia.Visuals/Media/DrawingContext.cs
  8. 11
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  9. 2
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  10. 10
      src/Avalonia.Visuals/Rect.cs
  11. 12
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  12. 13
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  13. 40
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  14. 136
      src/Avalonia.Visuals/RoundedRect.cs
  15. 122
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  16. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  17. 5
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  18. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  19. 13
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  20. 2
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  21. 2
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  22. 45
      tests/Avalonia.Visuals.UnitTests/Media/BoxShadowTests.cs
  23. 8
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  24. 8
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  25. 2
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

27
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -151,6 +151,31 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.InsetShadow">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green"/>
</KeyFrame>
<KeyFrame Cue="85%">
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -171,6 +196,8 @@
<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}" />
<Border Classes="Test InsetShadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test InsetShadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

2
src/Avalonia.Controls/Border.cs

@ -127,8 +127,6 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
}

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

@ -336,8 +336,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
return ArrangeOverrideImpl(finalSize, new Vector());
}

50
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<IPlatformRenderInterface>()
.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);
}
}

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

95
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<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]);
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);
}
}

4
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);
}
/// <summary>

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

@ -55,8 +55,7 @@ namespace Avalonia.Platform
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="boxShadow">The box shadow parameters</param>
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry, BoxShadow boxShadow);
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
@ -64,18 +63,12 @@ namespace Avalonia.Platform
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Width/2
/// </param>
/// <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, RoundedRect rect,
BoxShadow boxShadow = default);
/// <summary>

2
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -116,5 +116,7 @@ namespace Avalonia.Platform
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
bool SupportsIndividualRoundRects { get; }
}
}

10
src/Avalonia.Visuals/Rect.cs

@ -132,6 +132,16 @@ namespace Avalonia
/// Gets the bottom position of the rectangle.
/// </summary>
public double Bottom => _y + _height;
/// <summary>
/// Gets the left position.
/// </summary>
public double Left => _x;
/// <summary>
/// Gets the top position.
/// </summary>
public double Top => _y;
/// <summary>
/// Gets the top left point of the rectangle.

12
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, BoxShadow boxShadow)
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
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
}
/// <inheritdoc/>
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<RectangleNode>();
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
{

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

@ -19,20 +19,17 @@ 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>
/// <param name="boxShadow"></param>
public GeometryNode(Matrix transform,
IBrush brush,
IPen pen,
IGeometryImpl geometry,
BoxShadow boxShadow,
IDictionary<IVisual, Scene> 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
/// </summary>
public IGeometryImpl Geometry { get; }
public BoxShadow BoxShadow { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> 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.
/// </remarks>
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);
}
/// <inheritdoc/>

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

@ -19,26 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public RectangleNode(
Matrix transform,
IBrush brush,
IPen pen,
Rect rect,
double radiusX,
double radiusY,
RoundedRect rect,
BoxShadow boxShadow,
IDictionary<IVisual, Scene> 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
/// <summary>
/// Gets the rectangle to draw.
/// </summary>
public Rect Rect { get; }
/// <summary>
/// The radius in the X dimension of the rounded corners.
/// </summary>
public double RadiusX { get; }
/// <summary>
/// The radius in the Y dimension of the rounded corners.
/// </summary>
public double RadiusY { get; }
public RoundedRect Rect { get; }
/// <summary>
/// The parameters for the box-shadow effect
@ -88,22 +73,19 @@ 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="rect">The rectangle of the other draw operation.</param>
/// <param name="radiusX"></param>
/// <param name="radiusY"></param>
/// <param name="boxShadow">The box shadow parameters of the other draw operation</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, 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);
}
/// <inheritdoc/>
@ -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);
}
/// <inheritdoc/>
@ -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);
}
}

136
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]);
}
}
}

122
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

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

2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -247,5 +247,7 @@ namespace Avalonia.Skia
return new GlyphRunImpl(textBlob);
}
public bool SupportsIndividualRoundRects => true;
}
}

5
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)
{

2
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -240,5 +240,7 @@ namespace Avalonia.Direct2D1
return new GlyphRunImpl(run);
}
public bool SupportsIndividualRoundRects => false;
}
}

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

@ -199,8 +199,7 @@ 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,
BoxShadow boxShadow)
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
if (brush != null)
{
@ -229,10 +228,14 @@ namespace Avalonia.Direct2D1.Media
}
/// <inheritdoc />
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)

2
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -76,5 +76,7 @@ namespace Avalonia.Benchmarks
return new NullGlyphRun();
}
public bool SupportsIndividualRoundRects => true;
}
}

2
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -84,5 +84,7 @@ namespace Avalonia.UnitTests
width = 0;
return Mock.Of<IGlyphRunImpl>();
}
public bool SupportsIndividualRoundRects { get; set; }
}
}

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

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

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

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

Loading…
Cancel
Save