From 016ca4e617e8a693f177095e88c37006a94fbc99 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 17 Sep 2025 18:16:34 +0200 Subject: [PATCH 01/10] Simplify color converter for the XAML source generator --- .../TypeConverters/ColorConverter.cs | 76 ++----------------- 1 file changed, 6 insertions(+), 70 deletions(-) diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs index 8d86609df3eb..b8fbf21c5af9 100644 --- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs +++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs @@ -6,87 +6,23 @@ using Microsoft.CodeAnalysis; using Microsoft.Maui.Controls.Xaml; +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 (Maui.Graphics.Color.TryParse(value, out var color)) { - // 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}\")"; - } - } - - // 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}\")"; + var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!; + return $"new {colorType.ToFQDisplayString()}({FormatInvariant(color.Red)}, {FormatInvariant(color.Green)}, {FormatInvariant(color.Blue)}, {FormatInvariant(color.Alpha)}) /* {value} */"; // ensure double literals } - context.ReportConversionFailed(xmlLineInfo, value, toType, Descriptors.ConversionFailed); + context.ReportConversionFailed(node, value, toType, Descriptors.ConversionFailed); return "default"; } } \ No newline at end of file From 42fce2495179f6843a1438ac48aa2e0287263ad7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 17 Sep 2025 18:36:27 +0200 Subject: [PATCH 02/10] Fix test --- .../InitializeComponent/SimplifyOnPlatform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index 597cf954438f..59cd21b5083a 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -122,7 +122,7 @@ private partial void InitializeComponent() #line 8 "{{testXamlFilePath}}" setter.Value = "Pink"; #line default - var setter2 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Colors.Pink}; + var setter2 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = new global::Microsoft.Maui.Graphics.Color(1f, 0.7529412f, 0.79607844f, 1f) /* Pink */}; if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter2!) == null) global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter2!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 14); #line 8 "{{testXamlFilePath}}" From 097a265222f28527152927241d2e9616b9962a9e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 17 Sep 2025 18:36:40 +0200 Subject: [PATCH 03/10] Fix compilation --- src/Controls/src/SourceGen/Controls.SourceGen.csproj | 1 + src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/SourceGen/Controls.SourceGen.csproj b/src/Controls/src/SourceGen/Controls.SourceGen.csproj index 099ae547f6cc..3a32dcb086b1 100644 --- a/src/Controls/src/SourceGen/Controls.SourceGen.csproj +++ b/src/Controls/src/SourceGen/Controls.SourceGen.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs index b8fbf21c5af9..f9ac853c46f4 100644 --- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs +++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs @@ -5,6 +5,7 @@ using System.Xml; using Microsoft.CodeAnalysis; using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; using static Microsoft.Maui.Controls.SourceGen.GeneratorHelpers; @@ -16,10 +17,10 @@ internal class ColorConverter : ISGTypeConverter public string Convert(string value, BaseNode node, ITypeSymbol toType, SourceGenContext context, LocalVariable? parentVar = null) { - if (Maui.Graphics.Color.TryParse(value, out var color)) + if (Color.TryParse(value, out var color)) { var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!; - return $"new {colorType.ToFQDisplayString()}({FormatInvariant(color.Red)}, {FormatInvariant(color.Green)}, {FormatInvariant(color.Blue)}, {FormatInvariant(color.Alpha)}) /* {value} */"; // ensure double literals + return $"new {colorType.ToFQDisplayString()}({FormatInvariant(color.Red)}f, {FormatInvariant(color.Green)}f, {FormatInvariant(color.Blue)}f, {FormatInvariant(color.Alpha)}f) /* {value} */"; } context.ReportConversionFailed(node, value, toType, Descriptors.ConversionFailed); From 65d082a00947d187263ae9a92d31db75ae18917a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 17 Sep 2025 18:38:22 +0200 Subject: [PATCH 04/10] Bring back cast to IXmlLineInfo --- src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs index f9ac853c46f4..3d842c3540ac 100644 --- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs +++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs @@ -23,7 +23,7 @@ public string Convert(string value, BaseNode node, ITypeSymbol toType, SourceGen return $"new {colorType.ToFQDisplayString()}({FormatInvariant(color.Red)}f, {FormatInvariant(color.Green)}f, {FormatInvariant(color.Blue)}f, {FormatInvariant(color.Alpha)}f) /* {value} */"; } - context.ReportConversionFailed(node, value, toType, Descriptors.ConversionFailed); + context.ReportConversionFailed((IXmlLineInfo)node, value, toType, Descriptors.ConversionFailed); return "default"; } } \ No newline at end of file From 97739470d31a1853c244b81ce9ff219793635c27 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 18 Sep 2025 09:40:54 +0200 Subject: [PATCH 05/10] Move parsing and conversion functions to ColorUtils --- .../Platform/Android/TabbedPageManager.cs | 4 +- src/Graphics/src/Graphics/Color.cs | 462 +-------------- src/Graphics/src/Graphics/ColorUtils.cs | 543 ++++++++++++++++++ 3 files changed, 562 insertions(+), 447 deletions(-) create mode 100644 src/Graphics/src/Graphics/ColorUtils.cs 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/Graphics/src/Graphics/Color.cs b/src/Graphics/src/Graphics/Color.cs index 725cb28ad67c..f6ad5c97f581 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,36 @@ 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) + if (ColorUtils.TryParseRgbaHexFormat(colorAsHex, out float red, out float green, out float blue, out float alpha)) { - //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 new Color(red, green, blue, alpha); } - return FromRgba(red / 255f, green / 255f, blue / 255f, alpha / 255f); + return new Color(0, 0, 0, 1); } 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) + if (ColorUtils.TryParseHex(colorAsHex, out float red, out float green, out float blue, out float alpha)) { - //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 new Color(red, green, blue, alpha); } - return FromRgba(red / 255f, green / 255f, blue / 255f, alpha / 255f); + return new Color(0, 0, 0, 1); } 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); + (float r, float g, float 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); + (float r, float g, float 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 +386,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 +437,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 +459,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 +632,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..de23230b5791 --- /dev/null +++ b/src/Graphics/src/Graphics/ColorUtils.cs @@ -0,0 +1,543 @@ +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] == '#') + { + return TryParseHex(value, out red, out green, out blue, out alpha); + } + + 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; + } + + /// + /// Attempts to parse a hex color string (with or without #). + /// Supports formats: #RGB, #ARGB, #RRGGBB, #AARRGGBB + /// Uses ARGB format for 4-digit and 8-digit hex values. + /// + /// The hex color string + /// The red component (0.0-1.0) + /// The green component (0.0-1.0) + /// The blue component (0.0-1.0) + /// The alpha component (0.0-1.0) + /// True if parsing succeeded, false otherwise + public static bool TryParseHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) + { + red = green = blue = 0; + alpha = 1; + + if (colorAsHex.IsEmpty) + return false; + + //Skip # if present + if (colorAsHex[0] == '#') + colorAsHex = colorAsHex.Slice(1); + + try + { + return TryParseArgbHex(colorAsHex, out red, out green, out blue, out alpha); + } + catch + { + return false; + } + } + + /// + /// Attempts to parse a hex color string in RGBA format (with or without #). + /// Supports formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA + /// Uses RGBA format for 4-digit and 8-digit hex values. + /// + /// The hex color string + /// The red component (0.0-1.0) + /// The green component (0.0-1.0) + /// The blue component (0.0-1.0) + /// The alpha component (0.0-1.0) + /// True if parsing succeeded, false otherwise + public static bool TryParseRgbaHexFormat(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) + { + red = green = blue = 0; + alpha = 1; + + if (colorAsHex.IsEmpty) + return false; + + //Skip # if present + if (colorAsHex[0] == '#') + colorAsHex = colorAsHex.Slice(1); + + try + { + return TryParseRgbaHex(colorAsHex, out red, out green, out blue, out alpha); + } + catch + { + return false; + } + } + + /// + /// Parses hex color in ARGB format (#ARGB, #AARRGGBB) + /// + private static bool TryParseArgbHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) + { + int r = 0, g = 0, b = 0, a = 255; + + if (colorAsHex.Length == 6) + { + //#RRGGBB + r = ParseInt(colorAsHex.Slice(0, 2)); + g = ParseInt(colorAsHex.Slice(2, 2)); + b = ParseInt(colorAsHex.Slice(4, 2)); + } + else if (colorAsHex.Length == 3) + { + //#RGB + Span temp = stackalloc char[2]; + temp[0] = temp[1] = colorAsHex[0]; + r = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[1]; + g = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[2]; + b = ParseInt(temp); + } + else if (colorAsHex.Length == 4) + { + //#ARGB + Span temp = stackalloc char[2]; + temp[0] = temp[1] = colorAsHex[0]; + a = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[1]; + r = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[2]; + g = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[3]; + b = ParseInt(temp); + } + else if (colorAsHex.Length == 8) + { + //#AARRGGBB + a = ParseInt(colorAsHex.Slice(0, 2)); + r = ParseInt(colorAsHex.Slice(2, 2)); + g = ParseInt(colorAsHex.Slice(4, 2)); + b = ParseInt(colorAsHex.Slice(6, 2)); + } + else + { + red = green = blue = alpha = 0; + return false; + } + + red = (r / 255f).Clamp(0, 1); + green = (g / 255f).Clamp(0, 1); + blue = (b / 255f).Clamp(0, 1); + alpha = (a / 255f).Clamp(0, 1); + return true; + } + + /// + /// Parses hex color in RGBA format (#RGBA, #RRGGBBAA) + /// For 3-digit and 6-digit (no alpha), delegates to ARGB parsing since they're identical + /// + private static bool TryParseRgbaHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) + { + if (colorAsHex.Length == 3 || colorAsHex.Length == 6) + { + // For 3-digit and 6-digit hex, RGBA and ARGB are the same since there's no alpha + return TryParseArgbHex(colorAsHex, out red, out green, out blue, out alpha); + } + + int r = 0, g = 0, b = 0, a = 255; + + if (colorAsHex.Length == 4) + { + //#RGBA + Span temp = stackalloc char[2]; + temp[0] = temp[1] = colorAsHex[0]; + r = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[1]; + g = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[2]; + b = ParseInt(temp); + + temp[0] = temp[1] = colorAsHex[3]; + a = ParseInt(temp); + } + else if (colorAsHex.Length == 8) + { + //#RRGGBBAA + r = ParseInt(colorAsHex.Slice(0, 2)); + g = ParseInt(colorAsHex.Slice(2, 2)); + b = ParseInt(colorAsHex.Slice(4, 2)); + a = ParseInt(colorAsHex.Slice(6, 2)); + } + else + { + red = green = blue = alpha = 0; + return false; + } + + red = (r / 255f).Clamp(0, 1); + green = (g / 255f).Clamp(0, 1); + blue = (b / 255f).Clamp(0, 1); + alpha = (a / 255f).Clamp(0, 1); + return true; + } + + /// + /// 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]); + } + + /// + /// 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); +} From 29043f76609208fa226e31bebbb790224d9240bb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 18 Sep 2025 09:43:39 +0200 Subject: [PATCH 06/10] Use ColorUtils in the source generator --- .../src/SourceGen/Controls.SourceGen.csproj | 3 ++- .../SourceGen/TypeConverters/ColorConverter.cs | 18 ++++++++++++++++-- .../InitializeComponent/SimplifyOnPlatform.cs | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Controls/src/SourceGen/Controls.SourceGen.csproj b/src/Controls/src/SourceGen/Controls.SourceGen.csproj index 3a32dcb086b1..78b1e56eb1e4 100644 --- a/src/Controls/src/SourceGen/Controls.SourceGen.csproj +++ b/src/Controls/src/SourceGen/Controls.SourceGen.csproj @@ -52,6 +52,8 @@ Crc64HashAlgorithm.cs + + @@ -60,7 +62,6 @@ - diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs index 3d842c3540ac..81997958ed9b 100644 --- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs +++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs @@ -17,13 +17,27 @@ internal class ColorConverter : ISGTypeConverter public string Convert(string value, BaseNode node, ITypeSymbol toType, SourceGenContext context, LocalVariable? parentVar = null) { - if (Color.TryParse(value, out var color)) + if (ColorUtils.TryParse(value, out float red, out float green, out float blue, out float alpha)) { var colorType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color")!; - return $"new {colorType.ToFQDisplayString()}({FormatInvariant(color.Red)}f, {FormatInvariant(color.Green)}f, {FormatInvariant(color.Blue)}f, {FormatInvariant(color.Alpha)}f) /* {value} */"; + return $"new {colorType.ToFQDisplayString()}({FormatInvariant(red)}f, {FormatInvariant(green)}f, {FormatInvariant(blue)}f, {FormatInvariant(alpha)}f) /* {value} */"; + } + + if (GetNamedColorField(value) is IFieldSymbol colorsField) + { + return $"{colorsField.ContainingType.ToFQDisplayString()}.{colorsField.Name}"; } 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.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/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index 59cd21b5083a..597cf954438f 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -122,7 +122,7 @@ private partial void InitializeComponent() #line 8 "{{testXamlFilePath}}" setter.Value = "Pink"; #line default - var setter2 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = new global::Microsoft.Maui.Graphics.Color(1f, 0.7529412f, 0.79607844f, 1f) /* Pink */}; + var setter2 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Colors.Pink}; if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter2!) == null) global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter2!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 14); #line 8 "{{testXamlFilePath}}" From d59d8367458adc2916d9738a0c679bd7421c61ac Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 18 Sep 2025 12:58:39 +0200 Subject: [PATCH 07/10] Fix default colors --- src/Graphics/src/Graphics/Color.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Graphics/src/Graphics/Color.cs b/src/Graphics/src/Graphics/Color.cs index f6ad5c97f581..44a0e2f9ed25 100644 --- a/src/Graphics/src/Graphics/Color.cs +++ b/src/Graphics/src/Graphics/Color.cs @@ -344,7 +344,7 @@ static Color FromRgba(ReadOnlySpan colorAsHex) return new Color(red, green, blue, alpha); } - return new Color(0, 0, 0, 1); + return FromRgba(0f, 0f, 0f, 1f); } public static Color FromArgb(string colorAsHex) => FromArgb(colorAsHex != null ? colorAsHex.AsSpan() : default); @@ -356,7 +356,7 @@ static Color FromArgb(ReadOnlySpan colorAsHex) return new Color(red, green, blue, alpha); } - return new Color(0, 0, 0, 1); + return FromRgba(0f, 0f, 0f, 1f); } public static Color FromHsla(float h, float s, float l, float a = 1) From c523cfabc8c4b19ccf57de6f5610e0c9c0f4ed83 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 18 Sep 2025 13:28:37 +0200 Subject: [PATCH 08/10] Make parsing to be closer to the original implementation --- src/Graphics/src/Graphics/Color.cs | 20 +- src/Graphics/src/Graphics/ColorUtils.cs | 302 +++++++----------- .../tests/Graphics.Tests/ColorUnitTests.cs | 3 + 3 files changed, 128 insertions(+), 197 deletions(-) diff --git a/src/Graphics/src/Graphics/Color.cs b/src/Graphics/src/Graphics/Color.cs index 44a0e2f9ed25..7a0320334f80 100644 --- a/src/Graphics/src/Graphics/Color.cs +++ b/src/Graphics/src/Graphics/Color.cs @@ -339,35 +339,27 @@ public static Color FromRgba(double r, double g, double b, double a) static Color FromRgba(ReadOnlySpan colorAsHex) { - if (ColorUtils.TryParseRgbaHexFormat(colorAsHex, out float red, out float green, out float blue, out float alpha)) - { - return new Color(red, green, blue, alpha); - } - - return FromRgba(0f, 0f, 0f, 1f); + 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) { - if (ColorUtils.TryParseHex(colorAsHex, out float red, out float green, out float blue, out float alpha)) - { - return new Color(red, green, blue, alpha); - } - - return FromRgba(0f, 0f, 0f, 1f); + 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 r, float g, float b) = ColorUtils.ConvertHslToRgb(h, s, l); + 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 r, float g, float b) = ColorUtils.ConvertHslToRgb((float)h, (float)s, (float)l); + var (r, g, b) = ColorUtils.ConvertHslToRgb((float)h, (float)s, (float)l); return new Color(r, g, b, (float)a); } diff --git a/src/Graphics/src/Graphics/ColorUtils.cs b/src/Graphics/src/Graphics/ColorUtils.cs index de23230b5791..6ebcc8693375 100644 --- a/src/Graphics/src/Graphics/ColorUtils.cs +++ b/src/Graphics/src/Graphics/ColorUtils.cs @@ -15,7 +15,15 @@ public static bool TryParse(ReadOnlySpan value, out float red, out float g if (value[0] == '#') { - return TryParseHex(value, out red, out green, out blue, out alpha); + try + { + (red, green, blue, alpha) = FromArgb(value); + return true; + } + catch + { + return false; + } } if (value.StartsWith("rgba".AsSpan(), StringComparison.OrdinalIgnoreCase)) @@ -163,187 +171,6 @@ public static bool TryParse(ReadOnlySpan value, out float red, out float g return false; } - /// - /// Attempts to parse a hex color string (with or without #). - /// Supports formats: #RGB, #ARGB, #RRGGBB, #AARRGGBB - /// Uses ARGB format for 4-digit and 8-digit hex values. - /// - /// The hex color string - /// The red component (0.0-1.0) - /// The green component (0.0-1.0) - /// The blue component (0.0-1.0) - /// The alpha component (0.0-1.0) - /// True if parsing succeeded, false otherwise - public static bool TryParseHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) - { - red = green = blue = 0; - alpha = 1; - - if (colorAsHex.IsEmpty) - return false; - - //Skip # if present - if (colorAsHex[0] == '#') - colorAsHex = colorAsHex.Slice(1); - - try - { - return TryParseArgbHex(colorAsHex, out red, out green, out blue, out alpha); - } - catch - { - return false; - } - } - - /// - /// Attempts to parse a hex color string in RGBA format (with or without #). - /// Supports formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA - /// Uses RGBA format for 4-digit and 8-digit hex values. - /// - /// The hex color string - /// The red component (0.0-1.0) - /// The green component (0.0-1.0) - /// The blue component (0.0-1.0) - /// The alpha component (0.0-1.0) - /// True if parsing succeeded, false otherwise - public static bool TryParseRgbaHexFormat(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) - { - red = green = blue = 0; - alpha = 1; - - if (colorAsHex.IsEmpty) - return false; - - //Skip # if present - if (colorAsHex[0] == '#') - colorAsHex = colorAsHex.Slice(1); - - try - { - return TryParseRgbaHex(colorAsHex, out red, out green, out blue, out alpha); - } - catch - { - return false; - } - } - - /// - /// Parses hex color in ARGB format (#ARGB, #AARRGGBB) - /// - private static bool TryParseArgbHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) - { - int r = 0, g = 0, b = 0, a = 255; - - if (colorAsHex.Length == 6) - { - //#RRGGBB - r = ParseInt(colorAsHex.Slice(0, 2)); - g = ParseInt(colorAsHex.Slice(2, 2)); - b = ParseInt(colorAsHex.Slice(4, 2)); - } - else if (colorAsHex.Length == 3) - { - //#RGB - Span temp = stackalloc char[2]; - temp[0] = temp[1] = colorAsHex[0]; - r = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[1]; - g = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[2]; - b = ParseInt(temp); - } - else if (colorAsHex.Length == 4) - { - //#ARGB - Span temp = stackalloc char[2]; - temp[0] = temp[1] = colorAsHex[0]; - a = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[1]; - r = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[2]; - g = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[3]; - b = ParseInt(temp); - } - else if (colorAsHex.Length == 8) - { - //#AARRGGBB - a = ParseInt(colorAsHex.Slice(0, 2)); - r = ParseInt(colorAsHex.Slice(2, 2)); - g = ParseInt(colorAsHex.Slice(4, 2)); - b = ParseInt(colorAsHex.Slice(6, 2)); - } - else - { - red = green = blue = alpha = 0; - return false; - } - - red = (r / 255f).Clamp(0, 1); - green = (g / 255f).Clamp(0, 1); - blue = (b / 255f).Clamp(0, 1); - alpha = (a / 255f).Clamp(0, 1); - return true; - } - - /// - /// Parses hex color in RGBA format (#RGBA, #RRGGBBAA) - /// For 3-digit and 6-digit (no alpha), delegates to ARGB parsing since they're identical - /// - private static bool TryParseRgbaHex(ReadOnlySpan colorAsHex, out float red, out float green, out float blue, out float alpha) - { - if (colorAsHex.Length == 3 || colorAsHex.Length == 6) - { - // For 3-digit and 6-digit hex, RGBA and ARGB are the same since there's no alpha - return TryParseArgbHex(colorAsHex, out red, out green, out blue, out alpha); - } - - int r = 0, g = 0, b = 0, a = 255; - - if (colorAsHex.Length == 4) - { - //#RGBA - Span temp = stackalloc char[2]; - temp[0] = temp[1] = colorAsHex[0]; - r = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[1]; - g = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[2]; - b = ParseInt(temp); - - temp[0] = temp[1] = colorAsHex[3]; - a = ParseInt(temp); - } - else if (colorAsHex.Length == 8) - { - //#RRGGBBAA - r = ParseInt(colorAsHex.Slice(0, 2)); - g = ParseInt(colorAsHex.Slice(2, 2)); - b = ParseInt(colorAsHex.Slice(4, 2)); - a = ParseInt(colorAsHex.Slice(6, 2)); - } - else - { - red = green = blue = alpha = 0; - return false; - } - - red = (r / 255f).Clamp(0, 1); - green = (g / 255f).Clamp(0, 1); - blue = (b / 255f).Clamp(0, 1); - alpha = (a / 255f).Clamp(0, 1); - return true; - } - /// /// Converts HSL values to RGB. /// @@ -362,7 +189,7 @@ public static (float red, float green, float blue) ConvertHslToRgb(float hue, fl { return (luminosity, luminosity, luminosity); } - + float temp2 = luminosity <= 0.5f ? luminosity * (1.0f + saturation) : luminosity + saturation - luminosity * saturation; float temp1 = 2.0f * luminosity - temp2; @@ -387,6 +214,115 @@ public static (float red, float green, float blue) ConvertHslToRgb(float hue, fl 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. /// 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() From cb659388de23aeb15672566cb2d20f02723adc1d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 18 Sep 2025 13:33:17 +0200 Subject: [PATCH 09/10] Only consider public Colors fields --- src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs index 81997958ed9b..43ffbdbebdf2 100644 --- a/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs +++ b/src/Controls/src/SourceGen/TypeConverters/ColorConverter.cs @@ -36,7 +36,7 @@ public string Convert(string value, BaseNode node, ITypeSymbol toType, SourceGen return context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Colors") ?.GetMembers() .OfType() - .Where(f => f.IsStatic && f.IsReadOnly && f.Type.ToFQDisplayString() == "global::Microsoft.Maui.Graphics.Color") + .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)); } } From 156300f86e3578aec54713eab006ef238baf75cb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 20 Sep 2025 22:02:11 +0200 Subject: [PATCH 10/10] Fix RD test --- .../InitializeComponent/ResourceDictionary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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);