File: Platform\iOS\Extensions\FormattedStringExtensions.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
using CoreGraphics;
using System.Collections.Generic;
using Foundation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using System;
 
#if !MACOS
using ObjCRuntime;
using UIKit;
 
#else
using AppKit;
using UIColor = AppKit.NSColor;
using UITextAlignment = AppKit.NSTextAlignment;
#endif
 
namespace Microsoft.Maui.Controls.Platform
{
	public static class FormattedStringExtensions
	{
		public static NSAttributedString? ToNSAttributedString(this Label label)
			=> ToNSAttributedString(
				label.FormattedText,
				label.RequireFontManager(),
				label.LineHeight,
				label.HorizontalTextAlignment,
				label.ToFont(),
				label.TextColor,
				label.TextTransform);
 
		public static NSAttributedString ToNSAttributedString(
			this FormattedString formattedString,
			IFontManager fontManager,
			double defaultLineHeight = 0d, // TODO: NET8 should be -1, but too late to change for net6
			TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
			Font? defaultFont = null,
			Color? defaultColor = null,
			TextTransform defaultTextTransform = TextTransform.Default)
		{
			if (formattedString == null)
				return new NSAttributedString(string.Empty);
 
			var attributed = new NSMutableAttributedString();
			for (int i = 0; i < formattedString.Spans.Count; i++)
			{
				Span span = formattedString.Spans[i];
				if (span.Text == null)
					continue;
 
				attributed.Append(span.ToNSAttributedString(fontManager, defaultLineHeight, defaultHorizontalAlignment,
					defaultFont, defaultColor, defaultTextTransform));
			}
 
			return attributed;
		}
 
		public static NSAttributedString ToNSAttributedString(
			this Span span,
			IFontManager fontManager,
			double defaultLineHeight = 0d, // TODO: NET8 should be -1, but too late to change for NET8
			TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
			Font? defaultFont = null,
			Color? defaultColor = null,
			TextTransform defaultTextTransform = TextTransform.Default)
		{
			var defaultFontSize = defaultFont?.Size ?? fontManager.DefaultFontSize;
 
			var transform = span.TextTransform != TextTransform.Default ? span.TextTransform : defaultTextTransform;
 
			var text = TextTransformUtilites.GetTransformedText(span.Text, transform);
			if (text is null)
				return new NSAttributedString(string.Empty);
 
			var style = new NSMutableParagraphStyle();
			var lineHeight = span.LineHeight >= 0
				? span.LineHeight
				: defaultLineHeight;
 
			if (lineHeight >= 0)
			{
				style.LineHeightMultiple = new nfloat(lineHeight);
			}
 
			style.Alignment = defaultHorizontalAlignment switch
			{
				TextAlignment.Start => UITextAlignment.Left,
				TextAlignment.Center => UITextAlignment.Center,
				TextAlignment.End => UITextAlignment.Right,
				_ => UITextAlignment.Left
			};
 
			var font = span.ToFont(defaultFontSize);
			if (font.IsDefault && defaultFont.HasValue)
				font = defaultFont.Value;
 
			var hasUnderline = false;
			var hasStrikethrough = false;
			if (span.IsSet(Span.TextDecorationsProperty))
			{
				var textDecorations = span.TextDecorations;
				hasUnderline = (textDecorations & TextDecorations.Underline) != 0;
				hasStrikethrough = (textDecorations & TextDecorations.Strikethrough) != 0;
			}
 
			var platformFont = font.IsDefault ? null : font.ToUIFont(fontManager);
 
#if !MACOS
			var attrString = new NSAttributedString(
				text,
				platformFont,
				(span.TextColor ?? defaultColor)?.ToPlatform(),
				span.BackgroundColor?.ToPlatform(),
				underlineStyle: hasUnderline ? NSUnderlineStyle.Single : NSUnderlineStyle.None,
				strikethroughStyle: hasStrikethrough ? NSUnderlineStyle.Single : NSUnderlineStyle.None,
				paragraphStyle: style,
				kerning: (float)span.CharacterSpacing);
#else
			var attrString = new NSAttributedString(
				text,
				platformFont,
				(span.TextColor ?? defaultColor)?.ToPlatform(),
				span.BackgroundColor?.ToPlatform(),
				underlineStyle: hasUnderline ? NSUnderlineStyle.Single : NSUnderlineStyle.None,
				strikethroughStyle: hasStrikethrough ? NSUnderlineStyle.Single : NSUnderlineStyle.None,
				paragraphStyle: style,
				kerningAdjustment: (float)span.CharacterSpacing);
#endif
 
			return attrString;
		}
 
		internal static void RecalculateSpanPositions(this UILabel control, Label element)
		{
			if (element is null)
			{
				return;
			}
 
			if (element.TextType == TextType.Html)
			{
				return;
			}
 
			if (element?.FormattedText?.Spans is null
				|| element.FormattedText.Spans.Count == 0)
			{
				return;
			}
 
			var finalSize = control.Bounds;
 
			if (finalSize.Width <= 0 || finalSize.Height <= 0)
			{
				return;
			}
 
			var inline = control.AttributedText;
 
			if (inline is null)
			{
				return;
			}
 
			var attributedText = control.AttributedText;
			if (attributedText is null || attributedText.Length == 0)
			{
				return;
			}
 
			var spans = element.FormattedText.Spans;
 
			nint NSMaxRange(NSRange range) => range.Location + range.Length;
 
			using var textStorage = new NSTextStorage();
			using var layoutManager = new NSLayoutManager();
			using var textContainer = new NSTextContainer { LineFragmentPadding = 0 };
 
			textStorage.AddLayoutManager(layoutManager);
			layoutManager.AddTextContainer(textContainer);
 
			textContainer.Size = new(control.Bounds.Width,
				control.Lines == 0 ? nfloat.MaxValue : control.Bounds.Height);
 
			textStorage.SetString(attributedText);
			layoutManager.EnsureLayoutForTextContainer(textContainer);
 
			int currentLocation = 0;
 
			foreach (var span in spans)
			{
				if (string.IsNullOrEmpty(span?.Text))
				{
					continue;
				}
 
				var spanRects = new List<CGRect>();
				var spanStartIndex = currentLocation;
				var spanEndIndex = currentLocation + span.Text.Length - 1;
 
				var startGlyphRange = layoutManager.GetGlyphRange(new NSRange(spanStartIndex, 1));
				var endGlyphRange = layoutManager.GetGlyphRange(new NSRange(spanEndIndex, 1));
 
				void EnumerateLineFragmentCallback(CGRect rect, CGRect usedRect, NSTextContainer container,
					NSRange lineGlyphRange, out bool stop)
				{
					//Whole span is within the line and bigger than the line
					if (lineGlyphRange.Location >= startGlyphRange.Location &&
						NSMaxRange(lineGlyphRange) <= endGlyphRange.Location)
					{
						spanRects.Add(usedRect);
					}
					// Whole span is within the line and smaller than the line
					else if (lineGlyphRange.Location <= startGlyphRange.Location &&
							 endGlyphRange.Location <= NSMaxRange(lineGlyphRange))
					{
						var spanBoundingRect = layoutManager.GetBoundingRect(
							new(startGlyphRange.Location, endGlyphRange.Location - startGlyphRange.Location + 1),
							textContainer);
						spanRects.Add(spanBoundingRect);
					}
					// Span starts on current line and ends on next lines
					else if (lineGlyphRange.Location <= startGlyphRange.Location &&
							 NSMaxRange(lineGlyphRange) <= endGlyphRange.Location)
					{
						var spanBoundingRect = layoutManager.GetBoundingRect(
							new(startGlyphRange.Location, NSMaxRange(lineGlyphRange) - startGlyphRange.Location),
							textContainer);
						spanRects.Add(spanBoundingRect);
					}
					// Span starts on previous lines and ends on current line
					else if (lineGlyphRange.Location >= startGlyphRange.Location &&
							 NSMaxRange(lineGlyphRange) >= endGlyphRange.Location)
					{
						var spanBoundingRect = layoutManager.GetBoundingRect(
							new(lineGlyphRange.Location, endGlyphRange.Location - lineGlyphRange.Location),
							textContainer);
						spanRects.Add(spanBoundingRect);
					}
 
					stop = false;
				}
 
				layoutManager.EnumerateLineFragments(
					new(startGlyphRange.Location, endGlyphRange.Location - startGlyphRange.Location + 1),
					EnumerateLineFragmentCallback);
 
				if (span is ISpatialElement spatialElement)
				{
					var rects = new List<Rect>();
					foreach (var r in spanRects)
					{
						rects.Add(new Rect(r.X, r.Y, r.Width, r.Height));
					}
					spatialElement.Region = Region.FromRectangles(rects);
				}
				currentLocation += span.Text.Length;
			}
		}
	}
}