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;