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 TestFromArgbValuesHash() yield return new object[] { "#a222", Color.FromRgba(0x22, 0x22, 0x22, 0xaa) }; yield return new object[] { "#F2E2D2", Color.FromRgb(0xF2, 0xE2, 0xD2) }; yield return new object[] { "#C2F2E2D2", Color.FromRgba(0xF2, 0xE2, 0xD2, 0xC2) }; + yield return new object[] { "#000000", Color.FromRgba(0x00, 0x00, 0x00, 0xFF) }; + yield return new object[] { "#000", Color.FromRgba(0x00, 0x00, 0x00, 0xFF) }; + yield return new object[] { "#00FFff 40%", Color.FromRgba(0f, 0f, 0f, 1f) }; // unsupported syntax, but should not throw and fall back to the default black } public static IEnumerable TestFromArgbValuesNoHash()