diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs
index eaa886ccbd..cb90404f6d 100644
--- a/src/Avalonia.Base/Media/Color.cs
+++ b/src/Avalonia.Base/Media/Color.cs
@@ -166,7 +166,10 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
+
+ if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@@ -175,7 +178,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@@ -185,7 +188,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings
var str = s.ToString();
- if (s.Length > 5 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
+
+ if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@@ -238,7 +244,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@@ -248,7 +254,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@@ -271,6 +277,9 @@ namespace Avalonia.Media
return false;
}
+ ///
+ /// Parses the given span of characters representing a hex color value into a new .
+ ///
private static bool TryParseHexFormat(ReadOnlySpan s, out Color color)
{
static bool TryParseCore(ReadOnlySpan input, ref Color color)
@@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
+ ///
+ /// Parses the given string representing a CSS color value into a new .
+ ///
private static bool TryParseCssFormat(string s, out Color color)
{
+ bool prefixMatched = false;
+
color = default;
if (s is null)
@@ -342,27 +356,35 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
if (components.Length == 3) // RGB
{
- if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
- byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
- byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
+ if (InternalTryParseByte(components[0], out byte red) &&
+ InternalTryParseByte(components[1], out byte green) &&
+ InternalTryParseByte(components[2], out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
@@ -370,18 +392,45 @@ namespace Avalonia.Media
}
else if (components.Length == 4) // RGBA
{
- if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
- byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
- byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
- TryInternalParse(components[3], out double alpha))
+ if (InternalTryParseByte(components[0], out byte red) &&
+ InternalTryParseByte(components[1], out byte green) &&
+ InternalTryParseByte(components[2], out byte blue) &&
+ InternalTryParseDouble(components[3], out double alpha))
{
- color = new Color((byte)(alpha * 255), red, green, blue);
+ color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true;
}
}
+ // Local function to specially parse a byte value with an optional percentage sign
+ bool InternalTryParseByte(string inString, out byte outByte)
+ {
+ // The percent sign, if it exists, must be at the end of the number
+ int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outByte = (byte)Math.Round((percentage / 100.0) * 255.0);
+ return result;
+ }
+ else
+ {
+ return byte.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outByte);
+ }
+ }
+
// Local function to specially parse a double value with an optional percentage sign
- bool TryInternalParse(string inString, out double outDouble)
+ bool InternalTryParseDouble(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs
index e27a4f3106..e8a4d6f94f 100644
--- a/src/Avalonia.Base/Media/HslColor.cs
+++ b/src/Avalonia.Base/Media/HslColor.cs
@@ -12,6 +12,7 @@ namespace Avalonia.Media
{
///
/// Defines a color using the hue/saturation/lightness (HSL) model.
+ /// This uses a cylindrical-coordinate representation of a color.
///
#if !BUILDTASK
public
@@ -98,24 +99,53 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) component in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully transparent.
+ /// - 1 is fully opaque.
+ ///
+ ///
public double A { get; }
///
- /// Gets the Hue component in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360 (degrees).
+ /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
+ ///
+ ///
+ /// - 0/360 degrees is Red.
+ /// - 60 degrees is Yellow.
+ /// - 120 degrees is Green.
+ /// - 180 degrees is Cyan.
+ /// - 240 degrees is Blue.
+ /// - 300 degrees is Magenta.
+ ///
+ ///
public double H { get; }
///
- /// Gets the Saturation component in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is a shade of gray (no color).
+ /// - 1 is the full color.
+ ///
+ ///
public double S { get; }
///
- /// Gets the Lightness component in the range from 0..1.
+ /// Gets the Lightness component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully black.
+ /// - 1 is fully white.
+ ///
+ ///
public double L { get; }
///
@@ -226,6 +256,8 @@ namespace Avalonia.Media
/// True if parsing was successful; otherwise, false.
public static bool TryParse(string s, out HslColor hslColor)
{
+ bool prefixMatched = false;
+
hslColor = default;
if (s is null)
@@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible format is "hsl(0,0,0)", Length = 10.
+
+ if (workingString.Length >= 11 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs
index 164aeb1df1..924ef4778b 100644
--- a/src/Avalonia.Base/Media/HsvColor.cs
+++ b/src/Avalonia.Base/Media/HsvColor.cs
@@ -12,6 +12,7 @@ namespace Avalonia.Media
{
///
/// Defines a color using the hue/saturation/value (HSV) model.
+ /// This uses a cylindrical-coordinate representation of a color.
///
#if !BUILDTASK
public
@@ -98,24 +99,53 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) component in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully transparent.
+ /// - 1 is fully opaque.
+ ///
+ ///
public double A { get; }
///
- /// Gets the Hue component in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360 (degrees).
+ /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
+ ///
+ ///
+ /// - 0/360 degrees is Red.
+ /// - 60 degrees is Yellow.
+ /// - 120 degrees is Green.
+ /// - 180 degrees is Cyan.
+ /// - 240 degrees is Blue.
+ /// - 300 degrees is Magenta.
+ ///
+ ///
public double H { get; }
///
- /// Gets the Saturation component in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is a shade of gray (no color).
+ /// - 1 is the full color.
+ ///
+ ///
public double S { get; }
///
- /// Gets the Value component in the range from 0..1.
+ /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully black and shows no color.
+ /// - 1 is the brightest and shows full color.
+ ///
+ ///
public double V { get; }
///
@@ -226,6 +256,8 @@ namespace Avalonia.Media
/// True if parsing was successful; otherwise, false.
public static bool TryParse(string s, out HsvColor hsvColor)
{
+ bool prefixMatched = false;
+
hsvColor = default;
if (s is null)
@@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible format is "hsv(0,0,0)", Length = 10.
+
+ if (workingString.Length >= 11 &&
workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
diff --git a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
index 1392635b32..36929d5e95 100644
--- a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
@@ -216,8 +216,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsl(-1000, -1000, -1000)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)), // Clamps to min
- Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max
- Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max
+ Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
+ Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)),
@@ -262,8 +262,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsv(-1000, -1000, -1000)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)), // Clamps to min
- Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max
- Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max
+ Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
+ Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)),
@@ -303,8 +303,20 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)),
- Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)),
- Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)),
+ Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(230, 100, 30, 45)),
+ Tuple.Create("rgba(100, 30, 45, 90%)", new Color(230, 100, 30, 45)),
+
+ Tuple.Create("rgb(255,0,0)", new Color(255, 255, 0, 0)),
+ Tuple.Create("rgb(0,255,0)", new Color(255, 0, 255, 0)),
+ Tuple.Create("rgb(0,0,255)", new Color(255, 0, 0, 255)),
+
+ Tuple.Create("rgb(100%, 0, 0)", new Color(255, 255, 0, 0)),
+ Tuple.Create("rgb(0, 100%, 0)", new Color(255, 0, 255, 0)),
+ Tuple.Create("rgb(0, 0, 100%)", new Color(255, 0, 0, 255)),
+
+ Tuple.Create("rgba(0, 0, 100%, 50%)", new Color(128, 0, 0, 255)),
+ Tuple.Create("rgba(50%, 10%, 80%, 50%)", new Color(128, 128, 26, 204)),
+ Tuple.Create("rgba(50%, 10%, 80%, 0.5)", new Color(128, 128, 26, 204)),
// HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),