committed by
GitHub
29 changed files with 889 additions and 80 deletions
@ -0,0 +1,23 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Animation.Animators |
||||
|
{ |
||||
|
public class BoxShadowAnimator : Animator<BoxShadow> |
||||
|
{ |
||||
|
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 |
||||
|
{ |
||||
|
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), |
||||
|
IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
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; } |
||||
|
public bool IsInset { get; set; } |
||||
|
|
||||
|
static BoxShadow() |
||||
|
{ |
||||
|
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop => |
||||
|
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
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(); |
||||
|
|
||||
|
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(); |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
|
||||
|
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, |
||||
|
Spread = spread, |
||||
|
Color = color |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public Rect TransformBounds(in Rect rect) |
||||
|
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue