Browse Source

Support for multiple box-shadows

pull/3871/head
Nikita Tsukanov 6 years ago
parent
commit
bea8b676a2
  1. 34
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 6
      src/Avalonia.Controls/Border.cs
  3. 4
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 8
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  5. 40
      src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs
  6. 10
      src/Avalonia.Visuals/Media/BoxShadow.cs
  7. 137
      src/Avalonia.Visuals/Media/BoxShadows.cs
  8. 4
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  9. 6
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  10. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  11. 97
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  12. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

34
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -135,23 +135,6 @@
</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="100%">
<Setter Property="BoxShadow" Value="20 20 10 20 Green"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.InsetShadow">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Animations>
@ -159,20 +142,21 @@
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red"/>
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue"/>
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue, -15 20 0 0 Blue"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green"/>
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green, 20 20 20 0 Red"/>
</KeyFrame>
<KeyFrame Cue="85%">
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green"/>
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green, 20 20 20 10 Red"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green"/>
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
@ -194,10 +178,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}" />
<Border Classes="Test InsetShadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test InsetShadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

6
src/Avalonia.Controls/Border.cs

@ -36,8 +36,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadow> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadow>(nameof(BoxShadow));
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
@ -94,7 +94,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadow BoxShadow
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);

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

@ -43,7 +43,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadow> BoxShadowProperty =
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
Border.BoxShadowProperty.AddOwner<ContentPresenter>();
/// <summary>
@ -140,7 +140,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadow BoxShadow
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);

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

40
src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs

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

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

137
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<BoxShadowsAnimator>(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; c<Count ; c++)
if (!this[c].Equals(other[c]))
return false;
return true;
}
public override bool Equals(object obj)
{
return obj is BoxShadows other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = 0;
foreach (var s in this)
hashCode = (hashCode * 397) ^ s.GetHashCode();
return hashCode;
}
}
}
}

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

@ -63,13 +63,13 @@ 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="boxShadow">Box shadow effect parameters</param>
/// <param name="boxShadows">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, RoundedRect rect,
BoxShadow boxShadow = default);
BoxShadows boxShadow = default);
/// <summary>
/// Draws text.

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

@ -150,13 +150,13 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
BoxShadow boxShadow = default)
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
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
{

14
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<IVisual, Scene> 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;
}
/// <summary>
@ -61,7 +61,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// The parameters for the box-shadow effect
/// </summary>
public BoxShadow BoxShadow { get; }
public BoxShadows BoxShadows { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> 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.
/// </remarks>
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);
}
/// <inheritdoc/>

97
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
/// <inheritdoc />
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))

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

@ -228,7 +228,7 @@ namespace Avalonia.Direct2D1.Media
}
/// <inheritdoc />
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;

Loading…
Cancel
Save