29 changed files with 874 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