File: Brush\BrushTypeConverter.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Converters;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="Type[@FullName='Microsoft.Maui.Controls.BrushTypeConverter']/Docs/*" />
	[ProvideCompiled("Microsoft.Maui.Controls.XamlC.BrushTypeConverter")]
	public class BrushTypeConverter : TypeConverter
	{
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='LinearGradient']/Docs/*" />
		public const string LinearGradient = "linear-gradient";
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='RadialGradient']/Docs/*" />
		public const string RadialGradient = "radial-gradient";
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='Rgb']/Docs/*" />
		public const string Rgb = "rgb";
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='Rgba']/Docs/*" />
		public const string Rgba = "rgba";
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='Hsl']/Docs/*" />
		public const string Hsl = "hsl";
		/// <include file="../../docs/Microsoft.Maui.Controls/BrushTypeConverter.xml" path="//Member[@MemberName='Hsla']/Docs/*" />
		public const string Hsla = "hsla";
 
		readonly ColorTypeConverter _colorTypeConverter = new ColorTypeConverter();
 
		public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
			=> sourceType == typeof(string)
				|| sourceType == typeof(Color)
				|| sourceType == typeof(Paint);
 
		public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
			=> destinationType == typeof(Paint);
 
		public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
		{
			if (value is Color colorValue)
			{
				return (Brush)colorValue;
			}
			else if (value is Paint paintValue)
			{
				return (Brush)paintValue;
			}
 
			var strValue = value?.ToString();
 
			if (strValue != null)
			{
				strValue = strValue.Trim();
 
				if (strValue.StartsWith(LinearGradient) || strValue.StartsWith(RadialGradient))
				{
					var gradientBrushParser = new GradientBrushParser(_colorTypeConverter);
					var brush = gradientBrushParser.Parse(strValue);
 
					if (brush != null)
						return brush;
				}
 
				if (strValue.StartsWith(Rgb, StringComparison.InvariantCulture) || strValue.StartsWith(Rgba, StringComparison.InvariantCulture) || strValue.StartsWith(Hsl, StringComparison.InvariantCulture) || strValue.StartsWith(Hsla))
				{
					var color = (Color)_colorTypeConverter.ConvertFromInvariantString(strValue);
					return new SolidColorBrush(color);
				}
			}
 
			string[] parts = strValue.Split('.');
 
			if (parts.Length == 1 || (parts.Length == 2 && parts[0] == "Color"))
			{
				var color = (Color)_colorTypeConverter.ConvertFromInvariantString(strValue);
				return new SolidColorBrush(color);
			}
 
			return new SolidColorBrush(null);
		}
 
		public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
		{
			if (value is Brush brush && destinationType == typeof(Paint))
			{
				return (Paint)brush;
			}
 
			throw new NotSupportedException();
		}
 
		public class GradientBrushParser
		{
			readonly ColorTypeConverter _colorConverter;
			GradientBrush _gradient;
			string[] _parts;
			int _position;
 
			public GradientBrushParser(ColorTypeConverter colorConverter = null)
			{
				_colorConverter = colorConverter ?? new ColorTypeConverter();
			}
 
			public GradientBrush Parse(string css)
			{
				if (string.IsNullOrWhiteSpace(css))
				{
					return _gradient;
				}
 
#if NETSTANDARD2_0
				_parts = css.Replace("\r\n", "")
#else
				_parts = css.Replace("\r\n", "", StringComparison.Ordinal)
#endif
					.Split(new[] { '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries);
 
				while (_position < _parts.Length)
				{
					var part = GetPart().Trim();
 
					// Hex Color
					if (part.StartsWith("#", StringComparison.Ordinal))
					{
						var parts = part.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
						var color = (Color)_colorConverter.ConvertFromInvariantString(parts[0]);
 
						if (TryParseOffsets(parts, out var offsets))
							AddGradientStops(color, offsets);
						else
							AddGradientStop(color);
					}
 
					// Color by name
					var colorParts = part.Split('.');
					if (colorParts[0].Equals("Color", StringComparison.Ordinal))
					{
						var parts = part.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
						var color = (Color)_colorConverter.ConvertFromInvariantString(parts[0]);
 
						if (TryParseOffsets(parts, out var offsets))
							AddGradientStops(color, offsets);
						else
							AddGradientStop(color);
					}
 
					// Color (Rgb, Rgba, Hsl, Hsla)
					if (part.Equals(Rgb, StringComparison.OrdinalIgnoreCase)
						|| part.Equals(Rgba, StringComparison.OrdinalIgnoreCase)
						|| part.Equals(Hsl, StringComparison.OrdinalIgnoreCase)
						|| part.Equals(Hsla, StringComparison.OrdinalIgnoreCase))
					{
						var colorString = new StringBuilder(part);
 
						colorString.Append('(');
						colorString.Append(GetNextPart());
						colorString.Append(',');
						colorString.Append(GetNextPart());
						colorString.Append(',');
						colorString.Append(GetNextPart());
 
						if (part == Rgba || part == Hsla)
						{
							colorString.Append(',');
							colorString.Append(GetNextPart());
						}
 
						colorString.Append(')');
 
						var color = (Color)_colorConverter.ConvertFromInvariantString(colorString.ToString());
						var parts = GetNextPart().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
 
						if (TryParseOffsets(parts, out var offsets))
							AddGradientStops(color, offsets);
						else
						{
							AddGradientStop(color);
							_position--;
						}
					}
 
					// LinearGradient
					if (part == LinearGradient)
					{
						var direction = GetNextPart().Trim();
						var hasAngle = TryParseAngle(direction, out var angle);
 
						if (hasAngle)
							CreateLinearGradient(angle);
						else
						{
							CreateLinearGradient(0);
							_position--;
						}
					}
 
					// RadialGradient
					if (part == RadialGradient)
					{
						var center = GetGradientCenter();
						CreateRadialGradient(center);
					}
 
					_position++;
				}
 
				return _gradient;
			}
 
			string GetPart()
			{
				if (!(_position < _parts.Length))
					return string.Empty;
 
				return _parts[_position];
			}
 
			string GetNextPart()
			{
				_position++;
				return GetPart();
			}
 
			void CreateLinearGradient(double angle)
			{
				var coordinates = GetCoordinatesByAngle(angle);
				var startPoint = coordinates.Item1;
				var endPoint = coordinates.Item2;
 
				_gradient = new LinearGradientBrush
				{
					StartPoint = startPoint,
					EndPoint = endPoint,
					GradientStops = new GradientStopCollection()
				};
			}
 
			void CreateRadialGradient(Point center)
			{
				_gradient = new RadialGradientBrush
				{
					Center = center,
					GradientStops = new GradientStopCollection()
				};
			}
 
			void AddGradientStop(Color color, float? offset = null)
			{
				if (_gradient == null)
				{
					CreateLinearGradient(0);
				}
 
				var gradientStop = new GradientStop
				{
					Color = color,
					Offset = offset ?? -1
				};
 
				_gradient.GradientStops.Add(gradientStop);
			}
 
			void AddGradientStops(Color color, IEnumerable<float> offsets)
			{
				foreach (var offset in offsets)
					AddGradientStop(color, offset);
			}
 
			Tuple<Point, Point> GetCoordinatesByAngle(double angle)
			{
				Point startPoint;
				Point endPoint;
 
				switch (angle)
				{
					case 90:
						startPoint = new Point(0, 1);
						endPoint = new Point(0, 0);
						break;
					case 180:
						startPoint = new Point(1, 0);
						endPoint = new Point(0, 0);
						break;
					case 270:
						startPoint = new Point(0, 0);
						endPoint = new Point(0, 1);
						break;
					default:
					case 360:
						startPoint = new Point(0, 0);
						endPoint = new Point(1, 0);
						break;
				}
 
				return new Tuple<Point, Point>(startPoint, endPoint);
			}
 
			bool TryParseAngle(string part, out double angle)
			{
				if (TryParseNumber(part, "deg", out var degrees))
				{
					angle = degrees % 360;
					return true;
				}
 
				if (TryParseNumber(part, "turn", out var turn))
				{
					angle = 360 * turn;
					return true;
				}
 
				angle = 0;
				return false;
			}
 
			Point GetGradientCenter()
			{
				_position++;
 
				var part = GetPart().Trim();
 
				int gradientCenterPosition = 1;
				var parts = part.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
 
				if (parts.Length > gradientCenterPosition)
				{
					if (parts[gradientCenterPosition].IndexOf("at", StringComparison.Ordinal) != -1)
					{
						gradientCenterPosition++;
						var directionX = gradientCenterPosition < parts.Length ? parts[gradientCenterPosition].Trim() : string.Empty;
 
						gradientCenterPosition++;
						var directionY = gradientCenterPosition < parts.Length ? parts[gradientCenterPosition].Trim() : string.Empty;
 
						var hasPositionX = TryParseOffset(directionX, out var positionX);
						var hasPositionY = TryParseOffset(directionY, out var positionY);
 
						var position = new Point(0.5, 0.5);
 
						if (!hasPositionX && !string.IsNullOrEmpty(directionX))
							position = GetGradientPositionByDirection(directionX);
 
						if (!hasPositionY && !string.IsNullOrEmpty(directionY))
							position = GetGradientPositionByDirection(directionY);
 
						return new Point(hasPositionX ? positionX : position.X, hasPositionY ? positionY : position.Y);
					}
				}
 
				return new Point(0.5, 0.5);
			}
 
			Point GetGradientPositionByDirection(string direction)
			{
				switch (direction)
				{
					case "left":
						return new Point(0, 0.5);
					case "right":
						return new Point(1, 0.5);
					case "top":
						return new Point(0.5, 0);
					case "bottom":
						return new Point(0.5, 1);
					default:
					case "center":
						return new Point(0.5, 0.5);
				}
			}
 
			bool TryParseNumber(string part, string unit, out float result)
			{
				if (part.EndsWith(unit))
				{
					var index = part.LastIndexOf(unit, StringComparison.OrdinalIgnoreCase);
					var number = part.Substring(0, index);
 
					if (float.TryParse(number, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
					{
						result = value;
						return true;
					}
				}
 
				result = 0;
				return false;
			}
 
			bool TryParseOffset(string part, out float result)
			{
				if (part != null)
				{
					// Using percentage
					if (TryParseNumber(part, "%", out var value))
					{
						result = Math.Min(value / 100, 1f);
						return true;
					}
 
					// Using px
					if (TryParseNumber(part, "px", out result))
					{
						return true;
					}
				}
 
				result = 0;
				return false;
			}
 
			bool TryParseOffsets(string[] parts, out float[] result)
			{
				var offsets = new List<float>();
 
				foreach (var part in parts)
				{
					if (TryParseOffset(part, out var offset))
						offsets.Add(offset);
				}
 
				result = offsets.ToArray();
				return result.Length > 0;
			}
		}
	}
}