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;