diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index 2e06d2578f..052ee5e1b7 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -89,33 +89,64 @@ namespace Avalonia.Media
/// The .
public static Color Parse(string s)
{
- if (s == null) throw new ArgumentNullException(nameof(s));
- if (s.Length == 0) throw new FormatException();
+ if (TryParse(s, out Color color))
+ {
+ return color;
+ }
- if (s[0] == '#')
+ throw new FormatException($"Invalid color string: '{s}'.");
+ }
+
+ ///
+ /// Parses a color string.
+ ///
+ /// The color string.
+ /// The .
+ public static Color Parse(ReadOnlySpan s)
+ {
+ if (TryParse(s, out Color color))
{
- var or = 0u;
+ return color;
+ }
- if (s.Length == 7)
- {
- or = 0xff000000;
- }
- else if (s.Length != 9)
- {
- throw new FormatException($"Invalid color string: '{s}'.");
- }
+ throw new FormatException($"Invalid color string: '{s.ToString()}'.");
+ }
- return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or);
+ ///
+ /// Parses a color string.
+ ///
+ /// The color string.
+ /// The parsed color
+ /// The status of the operation.
+ public static bool TryParse(string s, out Color color)
+ {
+ if (s == null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (s.Length == 0)
+ {
+ throw new FormatException();
+ }
+
+ if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
+ {
+ return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
- return knownColor.ToColor();
+ color = knownColor.ToColor();
+
+ return true;
}
- throw new FormatException($"Invalid color string: '{s}'.");
+ color = default;
+
+ return false;
}
///
@@ -126,40 +157,79 @@ namespace Avalonia.Media
/// The status of the operation.
public static bool TryParse(ReadOnlySpan s, out Color color)
{
- color = default;
- if (s == null)
- return false;
if (s.Length == 0)
+ {
+ color = default;
+
return false;
+ }
if (s[0] == '#')
{
- var or = 0u;
+ return TryParseInternal(s, out color);
+ }
+
+ var knownColor = KnownColors.GetKnownColor(s.ToString());
+
+ if (knownColor != KnownColor.None)
+ {
+ color = knownColor.ToColor();
+
+ return true;
+ }
+
+ color = default;
+
+ return false;
+ }
+
+ private static bool TryParseInternal(ReadOnlySpan s, out Color color)
+ {
+ static bool TryParseCore(ReadOnlySpan input, ref Color color)
+ {
+ var alphaComponent = 0u;
- if (s.Length == 7)
+ if (input.Length == 6)
{
- or = 0xff000000;
+ alphaComponent = 0xff000000;
}
- else if (s.Length != 9)
+ else if (input.Length != 8)
{
return false;
}
- if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
+ // TODO: (netstandard 2.1) Can use allocation free parsing.
+ if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture,
+ out var parsed))
+ {
return false;
- color = FromUInt32(parsed| or);
+ }
+
+ color = FromUInt32(parsed | alphaComponent);
+
return true;
}
- var knownColor = KnownColors.GetKnownColor(s.ToString());
+ color = default;
- if (knownColor != KnownColor.None)
+ ReadOnlySpan input = s.Slice(1);
+
+ // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB).
+ if (input.Length == 3 || input.Length == 4)
{
- color = knownColor.ToColor();
- return true;
+ var extendedLength = 2 * input.Length;
+ Span extended = stackalloc char[extendedLength];
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ extended[2 * i + 0] = input[i];
+ extended[2 * i + 1] = input[i];
+ }
+
+ return TryParseCore(extended, ref color);
}
- return false;
+ return TryParseCore(input, ref color);
}
///
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
index e17fd47ff8..f3f3c9a4ca 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
@@ -17,6 +17,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void Try_Parse_Parses_RGB_Hash_Color()
+ {
+ var success = Color.TryParse("#ff8844", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
+ [Fact]
+ public void Parse_Parses_RGB_Hash_Shorthand_Color()
+ {
+ var result = Color.Parse("#f84");
+
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
+ [Fact]
+ public void Try_Parse_Parses_RGB_Hash_Shorthand_Color()
+ {
+ var success = Color.TryParse("#f84", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Parses_ARGB_Hash_Color()
{
@@ -28,6 +63,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0x40, result.A);
}
+ [Fact]
+ public void Try_Parse_Parses_ARGB_Hash_Color()
+ {
+ var success = Color.TryParse("#40ff8844", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x40, result.A);
+ }
+
+ [Fact]
+ public void Parse_Parses_ARGB_Hash_Shorthand_Color()
+ {
+ var result = Color.Parse("#4f84");
+
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x44, result.A);
+ }
+
+ [Fact]
+ public void Try_Parse_Parses_ARGB_Hash_Shorthand_Color()
+ {
+ var success = Color.TryParse("#4f84", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x44, result.A);
+ }
+
[Fact]
public void Parse_Parses_Named_Color_Lowercase()
{
@@ -39,6 +109,18 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void TryParse_Parses_Named_Color_Lowercase()
+ {
+ var success = Color.TryParse("red", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x00, result.G);
+ Assert.Equal(0x00, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Parses_Named_Color_Uppercase()
{
@@ -50,22 +132,52 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void TryParse_Parses_Named_Color_Uppercase()
+ {
+ var success = Color.TryParse("RED", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x00, result.G);
+ Assert.Equal(0x00, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
{
Assert.Throws(() => Color.Parse("#ff"));
}
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
+ {
+ Assert.False(Color.TryParse("#ff", out _));
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
{
Assert.Throws(() => Color.Parse("#ff5555555"));
}
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
+ {
+ Assert.False(Color.TryParse("#ff5555555", out _));
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Invalid_Number()
{
Assert.Throws(() => Color.Parse("#ff808g80"));
}
+
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Invalid_Number()
+ {
+ Assert.False(Color.TryParse("#ff808g80", out _));
+ }
}
}