diff --git a/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs b/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs
index e68e5b2c3590..8f6a3be91140 100644
--- a/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs
+++ b/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs
@@ -728,13 +728,13 @@ int GetDefaultColor()
// instead of leaving the application in a broken state
if (IsDarkTheme)
{
- defaultColor = ColorUtils.SetAlphaComponent(
+ defaultColor = AndroidX.Core.Graphics.ColorUtils.SetAlphaComponent(
ContextCompat.GetColor(_context.Context, Resource.Color.primary_dark_material_light),
153); // 60% opacity
}
else
{
- defaultColor = ColorUtils.SetAlphaComponent(
+ defaultColor = AndroidX.Core.Graphics.ColorUtils.SetAlphaComponent(
ContextCompat.GetColor(_context.Context, Resource.Color.primary_dark_material_dark),
153); // 60% opacity
}
diff --git a/src/Controls/src/SourceGen/Controls.SourceGen.csproj b/src/Controls/src/SourceGen/Controls.SourceGen.csproj
index 099ae547f6cc..78b1e56eb1e4 100644
--- a/src/Controls/src/SourceGen/Controls.SourceGen.csproj
+++ b/src/Controls/src/SourceGen/Controls.SourceGen.csproj
@@ -52,6 +52,8 @@
Crc64HashAlgorithm.cs
+
+
diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs
index 8d86609df3eb..43ffbdbebdf2 100644
--- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs
+++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs
@@ -5,88 +5,39 @@
using System.Xml;
using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.Xaml;
+using Microsoft.Maui.Graphics;
+
+using static Microsoft.Maui.Controls.SourceGen.GeneratorHelpers;
namespace Microsoft.Maui.Controls.SourceGen.TypeConverters;
internal class ColorConverter : ISGTypeConverter
{
- private static readonly HashSet KnownNamedColors = new(StringComparer.OrdinalIgnoreCase)
- {
- "AliceBlue", "AntiqueWhite", "Aqua", "Aquamarine", "Azure", "Beige", "Bisque", "Black",
- "BlanchedAlmond", "Blue", "BlueViolet", "Brown", "BurlyWood", "CadetBlue", "Chartreuse",
- "Chocolate", "Coral", "CornflowerBlue", "Cornsilk", "Crimson", "Cyan", "DarkBlue",
- "DarkCyan", "DarkGoldenrod", "DarkGray", "DarkGreen", "DarkGrey", "DarkKhaki",
- "DarkMagenta", "DarkOliveGreen", "DarkOrange", "DarkOrchid", "DarkRed", "DarkSalmon",
- "DarkSeaGreen", "DarkSlateBlue", "DarkSlateGray", "DarkSlateGrey", "DarkTurquoise",
- "DarkViolet", "DeepPink", "DeepSkyBlue", "DimGray", "DimGrey", "DodgerBlue", "Firebrick",
- "FloralWhite", "ForestGreen", "Fuchsia", "Gainsboro", "GhostWhite", "Gold", "Goldenrod",
- "Gray", "Green", "GreenYellow", "Grey", "Honeydew", "HotPink", "IndianRed", "Indigo",
- "Ivory", "Khaki", "Lavender", "LavenderBlush", "LawnGreen", "LemonChiffon", "LightBlue",
- "LightCoral", "LightCyan", "LightGoldenrodYellow", "LightGray", "LightGreen", "LightGrey",
- "LightPink", "LightSalmon", "LightSeaGreen", "LightSkyBlue", "LightSlateGray", "LightSlateGrey",
- "LightSteelBlue", "LightYellow", "Lime", "LimeGreen", "Linen", "Magenta", "Maroon",
- "MediumAquamarine", "MediumBlue", "MediumOrchid", "MediumPurple", "MediumSeaGreen",
- "MediumSlateBlue", "MediumSpringGreen", "MediumTurquoise", "MediumVioletRed", "MidnightBlue",
- "MintCream", "MistyRose", "Moccasin", "NavajoWhite", "Navy", "OldLace", "Olive", "OliveDrab",
- "Orange", "OrangeRed", "Orchid", "PaleGoldenrod", "PaleGreen", "PaleTurquoise", "PaleVioletRed",
- "PapayaWhip", "PeachPuff", "Peru", "Pink", "Plum", "PowderBlue", "Purple", "Red", "RosyBrown",
- "RoyalBlue", "SaddleBrown", "Salmon", "SandyBrown", "SeaGreen", "SeaShell", "Sienna", "Silver",
- "SkyBlue", "SlateBlue", "SlateGray", "SlateGrey", "Snow", "SpringGreen", "SteelBlue", "Tan",
- "Teal", "Thistle", "Tomato", "Transparent", "Turquoise", "Violet", "Wheat", "White",
- "WhiteSmoke", "Yellow", "YellowGreen"
- };
-
- // #rgb, #rrggbb, #aarrggbb are all valid
- private const string RxColorHexPattern = @"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}([0-9a-fA-F]{2})?)$";
- private static readonly Lazy RxColorHex = new(() => new Regex(RxColorHexPattern, RegexOptions.Compiled | RegexOptions.Singleline));
-
- // RGB, RGBA, HSL, HSLA, HSV, HSVA function patterns
- private const string RxFuncPattern = "^(?rgba|argb|rgb|hsla|hsl|hsva|hsv)\\(((?\\d%?),){2}((?\\d%?)|(?\\d%?),(?\\d%?))\\);?$";
- private static readonly Lazy RxFuncExpr = new(() => new Regex(RxFuncPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline));
-
public IEnumerable SupportedTypes => new[] { "Color", "Microsoft.Maui.Graphics.Color" };
public string Convert(string value, BaseNode node, ITypeSymbol toType, SourceGenContext context, LocalVariable? parentVar = null)
{
- var xmlLineInfo = (IXmlLineInfo)node;
- if (!string.IsNullOrEmpty(value))
+ if (ColorUtils.TryParse(value, out float red, out float green, out float blue, out float alpha))
{
- // Any named colors are ok. Surrounding white spaces are ok. Case insensitive.
- var actualColorName = KnownNamedColors.FirstOrDefault(c => string.Equals(c, value.Trim(), StringComparison.OrdinalIgnoreCase));
- if (actualColorName is not null)
- {
- var colorsType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Colors")!;
- return $"{colorsType.ToFQDisplayString()}.{actualColorName}";
- }
-
- // Check for HEX Color string
- if (RxColorHex.Value.IsMatch(value))
- {
- var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!;
- return $"{colorType.ToFQDisplayString()}.FromArgb(\"{value}\")";
- }
-
- var match = RxFuncExpr.Value.Match(value);
-
- var funcName = match?.Groups?["func"]?.Value;
- var funcValues = match?.Groups?["v"]?.Captures;
-
- if (!string.IsNullOrEmpty(funcName) && funcValues is not null)
- {
- // ie: argb() needs 4 parameters:
- if (funcValues.Count == funcName?.Length)
- {
- var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!;
- return $"{colorType.ToFQDisplayString()}.Parse(\"{value}\")";
- }
- }
+ var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!;
+ return $"new {colorType.ToFQDisplayString()}({FormatInvariant(red)}f, {FormatInvariant(green)}f, {FormatInvariant(blue)}f, {FormatInvariant(alpha)}f) /* {value} */";
+ }
- // As a last resort, try Color.Parse() for any other valid color formats
- var colorType2 = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!;
- return $"{colorType2.ToFQDisplayString()}.Parse(\"{value}\")";
+ if (GetNamedColorField(value) is IFieldSymbol colorsField)
+ {
+ return $"{colorsField.ContainingType.ToFQDisplayString()}.{colorsField.Name}";
}
- context.ReportConversionFailed(xmlLineInfo, value, toType, Descriptors.ConversionFailed);
+ context.ReportConversionFailed((IXmlLineInfo)node, value, toType, Descriptors.ConversionFailed);
return "default";
+
+ IFieldSymbol? GetNamedColorField(string name)
+ {
+ return context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Colors")
+ ?.GetMembers()
+ .OfType()
+ .Where(f => f.IsPublic() && f.IsStatic && f.IsReadOnly && f.Type.ToFQDisplayString() == "global::Microsoft.Maui.Graphics.Color")
+ .FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase));
+ }
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/ResourceDictionary.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/ResourceDictionary.cs
index 05c5d756eacc..2ebccb583076 100644
--- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/ResourceDictionary.cs
+++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/ResourceDictionary.cs
@@ -37,7 +37,7 @@ public partial class __TypeDBD64C1C77CDA760
{
private partial void InitializeComponent()
{
- var color = global::Microsoft.Maui.Graphics.Color.FromArgb("#FF4B14");
+ var color = new global::Microsoft.Maui.Graphics.Color(1f, 0.29411766f, 0.078431375f, 1f) /* #FF4B14 */;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color!, new global::System.Uri(@"Styles.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 6, 4);
var __root = this;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Styles.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
diff --git a/src/Graphics/src/Graphics/Color.cs b/src/Graphics/src/Graphics/Color.cs
index 725cb28ad67c..7a0320334f80 100644
--- a/src/Graphics/src/Graphics/Color.cs
+++ b/src/Graphics/src/Graphics/Color.cs
@@ -281,29 +281,8 @@ public Color GetComplementary()
public static Color FromHsva(float h, float s, float v, float a)
{
- h = h.Clamp(0, 1);
- s = s.Clamp(0, 1);
- v = v.Clamp(0, 1);
- var range = (int)(Math.Floor(h * 6)) % 6;
- var f = h * 6 - Math.Floor(h * 6);
- var p = v * (1 - s);
- var q = v * (1 - f * s);
- var t = v * (1 - (1 - f) * s);
-
- switch (range)
- {
- case 0:
- return FromRgba(v, t, p, a);
- case 1:
- return FromRgba(q, v, p, a);
- case 2:
- return FromRgba(p, v, t, a);
- case 3:
- return FromRgba(p, q, v, a);
- case 4:
- return FromRgba(t, p, v, a);
- }
- return FromRgba(v, p, q, a);
+ (float r, float g, float b) = ColorUtils.ConvertHsvToRgb(h, s, v);
+ return new Color(r, g, b, a);
}
public static Color FromUint(uint argb)
@@ -360,128 +339,28 @@ public static Color FromRgba(double r, double g, double b, double a)
static Color FromRgba(ReadOnlySpan colorAsHex)
{
- int red = 0;
- int green = 0;
- int blue = 0;
- int alpha = 255;
-
- if (!colorAsHex.IsEmpty)
- {
- //Skip # if present
- if (colorAsHex[0] == '#')
- colorAsHex = colorAsHex.Slice(1);
-
- if (colorAsHex.Length == 6 || colorAsHex.Length == 3)
- {
- //#RRGGBB or #RGB - since there is no A, use FromArgb
-
- return FromArgb(colorAsHex);
- }
- else if (colorAsHex.Length == 4)
- {
- //#RGBA
- Span temp = stackalloc char[2];
- temp[0] = temp[1] = colorAsHex[0];
- red = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[1];
- green = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[2];
- blue = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[3];
- alpha = ParseInt(temp);
- }
- else if (colorAsHex.Length == 8)
- {
- //#RRGGBBAA
- red = ParseInt(colorAsHex.Slice(0, 2));
- green = ParseInt(colorAsHex.Slice(2, 2));
- blue = ParseInt(colorAsHex.Slice(4, 2));
- alpha = ParseInt(colorAsHex.Slice(6, 2));
- }
- }
-
- return FromRgba(red / 255f, green / 255f, blue / 255f, alpha / 255f);
+ var (r, g, b, a) = ColorUtils.FromRgba(colorAsHex);
+ return new Color(r, g, b, a);
}
public static Color FromArgb(string colorAsHex) => FromArgb(colorAsHex != null ? colorAsHex.AsSpan() : default);
static Color FromArgb(ReadOnlySpan colorAsHex)
{
- int red = 0;
- int green = 0;
- int blue = 0;
- int alpha = 255;
-
- if (!colorAsHex.IsEmpty)
- {
- //Skip # if present
- if (colorAsHex[0] == '#')
- colorAsHex = colorAsHex.Slice(1);
-
- if (colorAsHex.Length == 6)
- {
- //#RRGGBB
- red = ParseInt(colorAsHex.Slice(0, 2));
- green = ParseInt(colorAsHex.Slice(2, 2));
- blue = ParseInt(colorAsHex.Slice(4, 2));
- }
- else if (colorAsHex.Length == 3)
- {
- //#RGB
- Span temp = stackalloc char[2];
- temp[0] = temp[1] = colorAsHex[0];
- red = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[1];
- green = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[2];
- blue = ParseInt(temp);
- }
- else if (colorAsHex.Length == 4)
- {
- //#ARGB
- Span temp = stackalloc char[2];
- temp[0] = temp[1] = colorAsHex[0];
- alpha = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[1];
- red = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[2];
- green = ParseInt(temp);
-
- temp[0] = temp[1] = colorAsHex[3];
- blue = ParseInt(temp);
- }
- else if (colorAsHex.Length == 8)
- {
- //#AARRGGBB
- alpha = ParseInt(colorAsHex.Slice(0, 2));
- red = ParseInt(colorAsHex.Slice(2, 2));
- green = ParseInt(colorAsHex.Slice(4, 2));
- blue = ParseInt(colorAsHex.Slice(6, 2));
- }
- }
-
- return FromRgba(red / 255f, green / 255f, blue / 255f, alpha / 255f);
+ var (r, g, b, a) = ColorUtils.FromArgb(colorAsHex);
+ return new Color(r, g, b, a);
}
public static Color FromHsla(float h, float s, float l, float a = 1)
{
- float red, green, blue;
- ConvertToRgb(h, s, l, out red, out green, out blue);
- return new Color(red, green, blue, a);
+ var (r, g, b) = ColorUtils.ConvertHslToRgb(h, s, l);
+ return new Color(r, g, b, a);
}
public static Color FromHsla(double h, double s, double l, double a = 1)
{
- float red, green, blue;
- ConvertToRgb((float)h, (float)s, (float)l, out red, out green, out blue);
- return new Color(red, green, blue, (float)a);
+ var (r, g, b) = ColorUtils.ConvertHslToRgb((float)h, (float)s, (float)l);
+ return new Color(r, g, b, (float)a);
}
public static Color FromHsv(float h, float s, float v)
@@ -499,45 +378,6 @@ public static Color FromHsv(int h, int s, int v)
return FromHsva(h / 360f, s / 100f, v / 100f, 1f);
}
- private static void ConvertToRgb(float hue, float saturation, float luminosity, out float r, out float g, out float b)
- {
- if (luminosity == 0)
- {
- r = g = b = 0;
- return;
- }
-
- if (saturation == 0)
- {
- r = g = b = luminosity;
- return;
- }
- float temp2 = luminosity <= 0.5f ? luminosity * (1.0f + saturation) : luminosity + saturation - luminosity * saturation;
- float temp1 = 2.0f * luminosity - temp2;
-
- var t3 = new[] { hue + 1.0f / 3.0f, hue, hue - 1.0f / 3.0f };
- var clr = new float[] { 0, 0, 0 };
- for (var i = 0; i < 3; i++)
- {
- if (t3[i] < 0)
- t3[i] += 1.0f;
- if (t3[i] > 1)
- t3[i] -= 1.0f;
- if (6.0 * t3[i] < 1.0)
- clr[i] = temp1 + (temp2 - temp1) * t3[i] * 6.0f;
- else if (2.0 * t3[i] < 1.0)
- clr[i] = temp2;
- else if (3.0 * t3[i] < 2.0)
- clr[i] = temp1 + (temp2 - temp1) * (2.0f / 3.0f - t3[i]) * 6.0f;
- else
- clr[i] = temp1;
- }
-
- r = clr[0];
- g = clr[1];
- b = clr[2];
- }
-
public void ToHsl(out float h, out float s, out float l)
{
var r = Red;
@@ -589,7 +429,6 @@ public void ToHsl(out float h, out float s, out float l)
h /= 6.0f;
}
-
// Supported inputs
// HEX #rgb, #argb, #rrggbb, #aarrggbb
// RGB rgb(255,0,0), rgb(100%,0%,0%) values in range 0-255 or 0%-100%
@@ -612,165 +451,14 @@ public static Color Parse(string value)
static bool TryParse(ReadOnlySpan value, out Color color)
{
- value = value.Trim();
- if (!value.IsEmpty)
+ if (ColorUtils.TryParse(value, out float red, out float green, out float blue, out float alpha))
{
- if (value[0] == '#')
- {
- try
- {
- color = Color.FromArgb(value);
- return true;
- }
- catch
- {
- goto ReturnFalse;
- }
- }
-
- if (value.StartsWith("rgba".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseFourColorRanges(value,
- out ReadOnlySpan quad0,
- out ReadOnlySpan quad1,
- out ReadOnlySpan quad2,
- out ReadOnlySpan quad3))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(quad0, 255, acceptPercent: true, out double r);
- valid &= TryParseColorValue(quad1, 255, acceptPercent: true, out double g);
- valid &= TryParseColorValue(quad2, 255, acceptPercent: true, out double b);
- valid &= TryParseOpacity(quad3, out double a);
-
- if (!valid)
- goto ReturnFalse;
-
- color = new Color((float)r, (float)g, (float)b, (float)a);
- return true;
- }
-
- if (value.StartsWith("rgb".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseThreeColorRanges(value,
- out ReadOnlySpan triplet0,
- out ReadOnlySpan triplet1,
- out ReadOnlySpan triplet2))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(triplet0, 255, acceptPercent: true, out double r);
- valid &= TryParseColorValue(triplet1, 255, acceptPercent: true, out double g);
- valid &= TryParseColorValue(triplet2, 255, acceptPercent: true, out double b);
-
- if (!valid)
- goto ReturnFalse;
-
- color = new Color((float)r, (float)g, (float)b);
- return true;
- }
-
- if (value.StartsWith("hsla".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseFourColorRanges(value,
- out ReadOnlySpan quad0,
- out ReadOnlySpan quad1,
- out ReadOnlySpan quad2,
- out ReadOnlySpan quad3))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(quad0, 360, acceptPercent: false, out double h);
- valid &= TryParseColorValue(quad1, 100, acceptPercent: true, out double s);
- valid &= TryParseColorValue(quad2, 100, acceptPercent: true, out double l);
- valid &= TryParseOpacity(quad3, out double a);
-
- if (!valid)
- goto ReturnFalse;
-
- color = Color.FromHsla(h, s, l, a);
- return true;
- }
-
- if (value.StartsWith("hsl".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseThreeColorRanges(value,
- out ReadOnlySpan triplet0,
- out ReadOnlySpan triplet1,
- out ReadOnlySpan triplet2))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(triplet0, 360, acceptPercent: false, out double h);
- valid &= TryParseColorValue(triplet1, 100, acceptPercent: true, out double s);
- valid &= TryParseColorValue(triplet2, 100, acceptPercent: true, out double l);
-
- if (!valid)
- goto ReturnFalse;
-
- color = Color.FromHsla(h, s, l);
- return true;
- }
-
- if (value.StartsWith("hsva".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseFourColorRanges(value,
- out ReadOnlySpan quad0,
- out ReadOnlySpan quad1,
- out ReadOnlySpan quad2,
- out ReadOnlySpan quad3))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(quad0, 360, acceptPercent: false, out double h);
- valid &= TryParseColorValue(quad1, 100, acceptPercent: true, out double s);
- valid &= TryParseColorValue(quad2, 100, acceptPercent: true, out double v);
- valid &= TryParseOpacity(quad3, out double a);
-
- if (!valid)
- goto ReturnFalse;
-
- color = Color.FromHsva((float)h, (float)s, (float)v, (float)a);
- return true;
- }
-
- if (value.StartsWith("hsv".AsSpan(), StringComparison.OrdinalIgnoreCase))
- {
- if (!TryParseThreeColorRanges(value,
- out ReadOnlySpan triplet0,
- out ReadOnlySpan triplet1,
- out ReadOnlySpan triplet2))
- {
- goto ReturnFalse;
- }
-
- bool valid = TryParseColorValue(triplet0, 360, acceptPercent: false, out double h);
- valid &= TryParseColorValue(triplet1, 100, acceptPercent: true, out double s);
- valid &= TryParseColorValue(triplet2, 100, acceptPercent: true, out double v);
-
- if (!valid)
- goto ReturnFalse;
-
- color = Color.FromHsv((float)h, (float)s, (float)v);
- return true;
- }
-
- var namedColor = GetNamedColor(value);
- if (namedColor != null)
- {
- color = namedColor;
- return true;
- }
+ color = new Color(red, green, blue, alpha);
+ return true;
}
- ReturnFalse:
- color = default;
- return false;
+ color = GetNamedColor(value);
+ return color is not null;
}
static Color GetNamedColor(ReadOnlySpan value)
@@ -936,130 +624,6 @@ static Color GetNamedColor(ReadOnlySpan value)
};
}
- static bool TryParseFourColorRanges(
- ReadOnlySpan value,
- out ReadOnlySpan quad0,
- out ReadOnlySpan quad1,
- out ReadOnlySpan quad2,
- out ReadOnlySpan quad3)
- {
- var op = value.IndexOf('(');
- var cp = value.LastIndexOf(')');
- if (op < 0 || cp < 0 || cp < op)
- goto ReturnFalse;
-
- value = value.Slice(op + 1, cp - op - 1);
-
- int index = value.IndexOf(',');
- if (index == -1)
- goto ReturnFalse;
- quad0 = value.Slice(0, index);
- value = value.Slice(index + 1);
-
- index = value.IndexOf(',');
- if (index == -1)
- goto ReturnFalse;
- quad1 = value.Slice(0, index);
- value = value.Slice(index + 1);
-
- index = value.IndexOf(',');
- if (index == -1)
- goto ReturnFalse;
- quad2 = value.Slice(0, index);
- quad3 = value.Slice(index + 1);
-
- // if there are more commas, fail
- if (quad3.IndexOf(',') != -1)
- goto ReturnFalse;
-
- return true;
-
- ReturnFalse:
- quad0 = quad1 = quad2 = quad3 = default;
- return false;
- }
-
- static bool TryParseThreeColorRanges(
- ReadOnlySpan value,
- out ReadOnlySpan triplet0,
- out ReadOnlySpan triplet1,
- out ReadOnlySpan triplet2)
- {
- var op = value.IndexOf('(');
- var cp = value.LastIndexOf(')');
- if (op < 0 || cp < 0 || cp < op)
- goto ReturnFalse;
-
- value = value.Slice(op + 1, cp - op - 1);
-
- int index = value.IndexOf(',');
- if (index == -1)
- goto ReturnFalse;
- triplet0 = value.Slice(0, index);
- value = value.Slice(index + 1);
-
- index = value.IndexOf(',');
- if (index == -1)
- goto ReturnFalse;
- triplet1 = value.Slice(0, index);
- triplet2 = value.Slice(index + 1);
-
- // if there are more commas, fail
- if (triplet2.IndexOf(',') != -1)
- goto ReturnFalse;
-
- return true;
-
- ReturnFalse:
- triplet0 = triplet1 = triplet2 = default;
- return false;
- }
-
- static bool TryParseColorValue(ReadOnlySpan elem, int maxValue, bool acceptPercent, out double value)
- {
- elem = elem.Trim();
- if (!elem.IsEmpty && elem[elem.Length - 1] == '%' && acceptPercent)
- {
- maxValue = 100;
- elem = elem.Slice(0, elem.Length - 1);
- }
-
- if (TryParseDouble(elem, out value))
- {
- value = value.Clamp(0, maxValue) / maxValue;
- return true;
- }
- return false;
- }
-
- static bool TryParseOpacity(ReadOnlySpan elem, out double value)
- {
- if (TryParseDouble(elem, out value))
- {
- value = value.Clamp(0, 1);
- return true;
- }
- return false;
- }
-
- static bool TryParseDouble(ReadOnlySpan s, out double value) =>
- double.TryParse(
-#if NETSTANDARD2_0 || TIZEN
- s.ToString(),
-#else
- s,
-#endif
- NumberStyles.Number, CultureInfo.InvariantCulture, out value);
-
- static int ParseInt(ReadOnlySpan s) =>
- int.Parse(
-#if NETSTANDARD2_0 || TIZEN
- s.ToString(),
-#else
- s,
-#endif
- NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
-
public static implicit operator Color(Vector4 color) => new Color(color);
}
}
diff --git a/src/Graphics/src/Graphics/ColorUtils.cs b/src/Graphics/src/Graphics/ColorUtils.cs
new file mode 100644
index 000000000000..6ebcc8693375
--- /dev/null
+++ b/src/Graphics/src/Graphics/ColorUtils.cs
@@ -0,0 +1,479 @@
+using System;
+using System.Globalization;
+
+namespace Microsoft.Maui.Graphics;
+
+internal static class ColorUtils
+{
+ public static bool TryParse(ReadOnlySpan value, out float red, out float green, out float blue, out float alpha)
+ {
+ red = green = blue = alpha = 0f;
+
+ value = value.Trim();
+ if (value.IsEmpty)
+ return false;
+
+ if (value[0] == '#')
+ {
+ try
+ {
+ (red, green, blue, alpha) = FromArgb(value);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ if (value.StartsWith("rgba".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseFourColorRanges(value,
+ out ReadOnlySpan quad0,
+ out ReadOnlySpan quad1,
+ out ReadOnlySpan quad2,
+ out ReadOnlySpan quad3))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(quad0, 255, acceptPercent: true, out double r);
+ valid &= TryParseColorValue(quad1, 255, acceptPercent: true, out double g);
+ valid &= TryParseColorValue(quad2, 255, acceptPercent: true, out double b);
+ valid &= TryParseOpacity(quad3, out double a);
+
+ if (!valid)
+ return false;
+
+ red = (float)r;
+ green = (float)g;
+ blue = (float)b;
+ alpha = (float)a;
+ return true;
+ }
+
+ if (value.StartsWith("rgb".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseThreeColorRanges(value,
+ out ReadOnlySpan triplet0,
+ out ReadOnlySpan triplet1,
+ out ReadOnlySpan triplet2))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(triplet0, 255, acceptPercent: true, out double r);
+ valid &= TryParseColorValue(triplet1, 255, acceptPercent: true, out double g);
+ valid &= TryParseColorValue(triplet2, 255, acceptPercent: true, out double b);
+
+ if (!valid)
+ return false;
+
+ red = (float)r;
+ green = (float)g;
+ blue = (float)b;
+ alpha = 1.0f;
+ return true;
+ }
+
+ if (value.StartsWith("hsla".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseFourColorRanges(value,
+ out ReadOnlySpan quad0,
+ out ReadOnlySpan quad1,
+ out ReadOnlySpan quad2,
+ out ReadOnlySpan quad3))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(quad0, 360, acceptPercent: false, out double h);
+ valid &= TryParseColorValue(quad1, 100, acceptPercent: true, out double s);
+ valid &= TryParseColorValue(quad2, 100, acceptPercent: true, out double l);
+ valid &= TryParseOpacity(quad3, out double a);
+
+ if (!valid)
+ return false;
+
+ (red, green, blue) = ConvertHslToRgb((float)h, (float)s, (float)l);
+ alpha = (float)a;
+ return true;
+ }
+
+ if (value.StartsWith("hsl".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseThreeColorRanges(value,
+ out ReadOnlySpan triplet0,
+ out ReadOnlySpan triplet1,
+ out ReadOnlySpan triplet2))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(triplet0, 360, acceptPercent: false, out double h);
+ valid &= TryParseColorValue(triplet1, 100, acceptPercent: true, out double s);
+ valid &= TryParseColorValue(triplet2, 100, acceptPercent: true, out double l);
+
+ if (!valid)
+ return false;
+
+ (red, green, blue) = ConvertHslToRgb((float)h, (float)s, (float)l);
+ alpha = 1.0f;
+ return true;
+ }
+
+ if (value.StartsWith("hsva".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseFourColorRanges(value,
+ out ReadOnlySpan quad0,
+ out ReadOnlySpan quad1,
+ out ReadOnlySpan quad2,
+ out ReadOnlySpan quad3))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(quad0, 360, acceptPercent: false, out double h);
+ valid &= TryParseColorValue(quad1, 100, acceptPercent: true, out double s);
+ valid &= TryParseColorValue(quad2, 100, acceptPercent: true, out double v);
+ valid &= TryParseOpacity(quad3, out double a);
+
+ if (!valid)
+ return false;
+
+ (red, green, blue) = ConvertHsvToRgb((float)h, (float)s, (float)v);
+ alpha = (float)a;
+ return true;
+ }
+
+ if (value.StartsWith("hsv".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!TryParseThreeColorRanges(value,
+ out ReadOnlySpan triplet0,
+ out ReadOnlySpan triplet1,
+ out ReadOnlySpan triplet2))
+ {
+ return false;
+ }
+
+ bool valid = TryParseColorValue(triplet0, 360, acceptPercent: false, out double h);
+ valid &= TryParseColorValue(triplet1, 100, acceptPercent: true, out double s);
+ valid &= TryParseColorValue(triplet2, 100, acceptPercent: true, out double v);
+
+ if (!valid)
+ return false;
+
+ (red, green, blue) = ConvertHsvToRgb((float)h, (float)s, (float)v);
+ alpha = 1.0f;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Converts HSL values to RGB.
+ ///
+ /// Hue (0.0-1.0)
+ /// Saturation (0.0-1.0)
+ /// Luminosity (0.0-1.0)
+ /// RGB components as (red, green, blue) where each component is 0.0-1.0
+ public static (float red, float green, float blue) ConvertHslToRgb(float hue, float saturation, float luminosity)
+ {
+ if (luminosity == 0)
+ {
+ return (0, 0, 0);
+ }
+
+ if (saturation == 0)
+ {
+ return (luminosity, luminosity, luminosity);
+ }
+
+ float temp2 = luminosity <= 0.5f ? luminosity * (1.0f + saturation) : luminosity + saturation - luminosity * saturation;
+ float temp1 = 2.0f * luminosity - temp2;
+
+ var t3 = new[] { hue + 1.0f / 3.0f, hue, hue - 1.0f / 3.0f };
+ var clr = new float[] { 0, 0, 0 };
+ for (var i = 0; i < 3; i++)
+ {
+ if (t3[i] < 0)
+ t3[i] += 1.0f;
+ if (t3[i] > 1)
+ t3[i] -= 1.0f;
+ if (6.0 * t3[i] < 1.0)
+ clr[i] = temp1 + (temp2 - temp1) * t3[i] * 6.0f;
+ else if (2.0 * t3[i] < 1.0)
+ clr[i] = temp2;
+ else if (3.0 * t3[i] < 2.0)
+ clr[i] = temp1 + (temp2 - temp1) * (2.0f / 3.0f - t3[i]) * 6.0f;
+ else
+ clr[i] = temp1;
+ }
+
+ return (clr[0], clr[1], clr[2]);
+ }
+
+ public static (float red, float green, float blue, float alpha) FromRgba(ReadOnlySpan colorAsHex)
+ {
+ int red = 0;
+ int green = 0;
+ int blue = 0;
+ int alpha = 255;
+
+ if (!colorAsHex.IsEmpty)
+ {
+ //Skip # if present
+ if (colorAsHex[0] == '#')
+ colorAsHex = colorAsHex.Slice(1);
+
+ if (colorAsHex.Length == 6 || colorAsHex.Length == 3)
+ {
+ //#RRGGBB or #RGB - since there is no A, use FromArgb
+
+ return FromArgb(colorAsHex);
+ }
+ else if (colorAsHex.Length == 4)
+ {
+ //#RGBA
+ Span temp = stackalloc char[2];
+ temp[0] = temp[1] = colorAsHex[0];
+ red = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[1];
+ green = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[2];
+ blue = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[3];
+ alpha = ParseInt(temp);
+ }
+ else if (colorAsHex.Length == 8)
+ {
+ //#RRGGBBAA
+ red = ParseInt(colorAsHex.Slice(0, 2));
+ green = ParseInt(colorAsHex.Slice(2, 2));
+ blue = ParseInt(colorAsHex.Slice(4, 2));
+ alpha = ParseInt(colorAsHex.Slice(6, 2));
+ }
+ }
+
+ return (red / 255f, green / 255f, blue / 255f, alpha / 255f);
+ }
+
+ public static (float red, float green, float blue, float alpha) FromArgb(ReadOnlySpan colorAsHex)
+ {
+ int red = 0;
+ int green = 0;
+ int blue = 0;
+ int alpha = 255;
+
+ if (!colorAsHex.IsEmpty)
+ {
+ //Skip # if present
+ if (colorAsHex[0] == '#')
+ colorAsHex = colorAsHex.Slice(1);
+
+ if (colorAsHex.Length == 6)
+ {
+ //#RRGGBB
+ red = ParseInt(colorAsHex.Slice(0, 2));
+ green = ParseInt(colorAsHex.Slice(2, 2));
+ blue = ParseInt(colorAsHex.Slice(4, 2));
+ }
+ else if (colorAsHex.Length == 3)
+ {
+ //#RGB
+ Span temp = stackalloc char[2];
+ temp[0] = temp[1] = colorAsHex[0];
+ red = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[1];
+ green = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[2];
+ blue = ParseInt(temp);
+ }
+ else if (colorAsHex.Length == 4)
+ {
+ //#ARGB
+ Span temp = stackalloc char[2];
+ temp[0] = temp[1] = colorAsHex[0];
+ alpha = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[1];
+ red = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[2];
+ green = ParseInt(temp);
+
+ temp[0] = temp[1] = colorAsHex[3];
+ blue = ParseInt(temp);
+ }
+ else if (colorAsHex.Length == 8)
+ {
+ //#AARRGGBB
+ alpha = ParseInt(colorAsHex.Slice(0, 2));
+ red = ParseInt(colorAsHex.Slice(2, 2));
+ green = ParseInt(colorAsHex.Slice(4, 2));
+ blue = ParseInt(colorAsHex.Slice(6, 2));
+ }
+ }
+
+ return (red / 255f, green / 255f, blue / 255f, alpha / 255f);
+ }
+ ///
+ /// Converts HSV values to RGB.
+ ///
+ /// Hue (0.0-1.0)
+ /// Saturation (0.0-1.0)
+ /// Value (0.0-1.0)
+ /// RGB components as (red, green, blue) where each component is 0.0-1.0
+ public static (float red, float green, float blue) ConvertHsvToRgb(float h, float s, float v)
+ {
+ h = h.Clamp(0, 1);
+ s = s.Clamp(0, 1);
+ v = v.Clamp(0, 1);
+
+ var range = (int)(Math.Floor(h * 6)) % 6;
+ var f = h * 6 - Math.Floor(h * 6);
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ return range switch
+ {
+ 0 => (v, (float)t, (float)p),
+ 1 => ((float)q, v, (float)p),
+ 2 => ((float)p, v, (float)t),
+ 3 => ((float)p, (float)q, v),
+ 4 => ((float)t, (float)p, v),
+ _ => (v, (float)p, (float)q)
+ };
+ }
+
+ private static bool TryParseFourColorRanges(
+ ReadOnlySpan value,
+ out ReadOnlySpan quad0,
+ out ReadOnlySpan quad1,
+ out ReadOnlySpan quad2,
+ out ReadOnlySpan quad3)
+ {
+ var op = value.IndexOf('(');
+ var cp = value.LastIndexOf(')');
+ if (op < 0 || cp < 0 || cp < op)
+ goto ReturnFalse;
+
+ value = value.Slice(op + 1, cp - op - 1);
+
+ int index = value.IndexOf(',');
+ if (index == -1)
+ goto ReturnFalse;
+ quad0 = value.Slice(0, index);
+ value = value.Slice(index + 1);
+
+ index = value.IndexOf(',');
+ if (index == -1)
+ goto ReturnFalse;
+ quad1 = value.Slice(0, index);
+ value = value.Slice(index + 1);
+
+ index = value.IndexOf(',');
+ if (index == -1)
+ goto ReturnFalse;
+ quad2 = value.Slice(0, index);
+ quad3 = value.Slice(index + 1);
+
+ // if there are more commas, fail
+ if (quad3.IndexOf(',') != -1)
+ goto ReturnFalse;
+
+ return true;
+
+ ReturnFalse:
+ quad0 = quad1 = quad2 = quad3 = default;
+ return false;
+ }
+
+ private static bool TryParseThreeColorRanges(
+ ReadOnlySpan value,
+ out ReadOnlySpan triplet0,
+ out ReadOnlySpan triplet1,
+ out ReadOnlySpan triplet2)
+ {
+ var op = value.IndexOf('(');
+ var cp = value.LastIndexOf(')');
+ if (op < 0 || cp < 0 || cp < op)
+ goto ReturnFalse;
+
+ value = value.Slice(op + 1, cp - op - 1);
+
+ int index = value.IndexOf(',');
+ if (index == -1)
+ goto ReturnFalse;
+ triplet0 = value.Slice(0, index);
+ value = value.Slice(index + 1);
+
+ index = value.IndexOf(',');
+ if (index == -1)
+ goto ReturnFalse;
+ triplet1 = value.Slice(0, index);
+ triplet2 = value.Slice(index + 1);
+
+ // if there are more commas, fail
+ if (triplet2.IndexOf(',') != -1)
+ goto ReturnFalse;
+
+ return true;
+
+ ReturnFalse:
+ triplet0 = triplet1 = triplet2 = default;
+ return false;
+ }
+
+ private static bool TryParseColorValue(ReadOnlySpan elem, int maxValue, bool acceptPercent, out double value)
+ {
+ elem = elem.Trim();
+ if (!elem.IsEmpty && elem[elem.Length - 1] == '%' && acceptPercent)
+ {
+ maxValue = 100;
+ elem = elem.Slice(0, elem.Length - 1);
+ }
+
+ if (TryParseDouble(elem, out value))
+ {
+ value = value.Clamp(0, maxValue) / maxValue;
+ return true;
+ }
+ return false;
+ }
+
+ private static bool TryParseOpacity(ReadOnlySpan elem, out double value)
+ {
+ if (TryParseDouble(elem, out value))
+ {
+ value = value.Clamp(0, 1);
+ return true;
+ }
+ return false;
+ }
+
+ private static bool TryParseDouble(ReadOnlySpan s, out double value) =>
+ double.TryParse(
+#if NETSTANDARD2_0 || TIZEN
+ s.ToString(),
+#else
+ s,
+#endif
+ NumberStyles.Number, CultureInfo.InvariantCulture, out value);
+
+ private static int ParseInt(ReadOnlySpan s) =>
+ int.Parse(
+#if NETSTANDARD2_0 || TIZEN
+ s.ToString(),
+#else
+ s,
+#endif
+ NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
+}
diff --git a/src/Graphics/tests/Graphics.Tests/ColorUnitTests.cs b/src/Graphics/tests/Graphics.Tests/ColorUnitTests.cs
index fe63e0bb2bcb..edaba8d32657 100644
--- a/src/Graphics/tests/Graphics.Tests/ColorUnitTests.cs
+++ b/src/Graphics/tests/Graphics.Tests/ColorUnitTests.cs
@@ -372,6 +372,9 @@ public static IEnumerable