Browse Source

Merge pull request #7993 from robloo/advanced-colors-2

Minor Color Updates
pull/7994/head
Max Katz 5 years ago
committed by GitHub
parent
commit
578de7ebbc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 83
      src/Avalonia.Base/Media/Color.cs
  2. 55
      src/Avalonia.Base/Media/HslColor.cs
  3. 55
      src/Avalonia.Base/Media/HsvColor.cs
  4. 24
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

83
src/Avalonia.Base/Media/Color.cs

@ -166,7 +166,10 @@ namespace Avalonia.Media
return true; 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -175,7 +178,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -185,7 +188,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings // At this point all parsing uses strings
var str = s.ToString(); 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -238,7 +244,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -248,7 +254,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -271,6 +277,9 @@ namespace Avalonia.Media
return false; return false;
} }
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color) private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{ {
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color) static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color); return TryParseCore(input, ref color);
} }
/// <summary>
/// Parses the given string representing a CSS color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseCssFormat(string s, out Color color) private static bool TryParseCssFormat(string s, out Color color)
{ {
bool prefixMatched = false;
color = default; color = default;
if (s is null) if (s is null)
@ -342,27 +356,35 @@ namespace Avalonia.Media
return false; return false;
} }
if (workingString.Length > 6 && if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');
if (components.Length == 3) // RGB if (components.Length == 3) // RGB
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue)) InternalTryParseByte(components[2], out byte blue))
{ {
color = new Color(0xFF, red, green, blue); color = new Color(0xFF, red, green, blue);
return true; return true;
@ -370,18 +392,45 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // RGBA else if (components.Length == 4) // RGBA
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) && InternalTryParseByte(components[2], out byte blue) &&
TryInternalParse(components[3], out double alpha)) 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; 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 // 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 // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);

55
src/Avalonia.Base/Media/HslColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model. /// Defines a color using the hue/saturation/lightness (HSL) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1. /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <summary>
/// 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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; } public double S { get; }
/// <summary> /// <summary>
/// Gets the Lightness component in the range from 0..1. /// Gets the Lightness component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black.</item>
/// <item>1 is fully white.</item>
/// </list>
/// </remarks>
public double L { get; } public double L { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor) public static bool TryParse(string s, out HslColor hslColor)
{ {
bool prefixMatched = false;
hslColor = default; hslColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; 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.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

55
src/Avalonia.Base/Media/HsvColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/value (HSV) model. /// Defines a color using the hue/saturation/value (HSV) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1. /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <summary>
/// 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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; } public double S { get; }
/// <summary> /// <summary>
/// Gets the Value component in the range from 0..1. /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black and shows no color.</item>
/// <item>1 is the brightest and shows full color.</item>
/// </list>
/// </remarks>
public double V { get; } public double V { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor) public static bool TryParse(string s, out HsvColor hsvColor)
{ {
bool prefixMatched = false;
hsvColor = default; hsvColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; 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.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

24
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, 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 (Hue wraps to zero)
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(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)), 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)), 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, 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 (Hue wraps to zero)
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(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)), 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)), 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("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)), 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, 0.9)", new Color(230, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 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 // HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)), Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),

Loading…
Cancel
Save