File: iOS\Renderers\LabelRenderer.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.ComponentModel;
using RectangleF = CoreGraphics.CGRect;
using SizeF = CoreGraphics.CGSize;
using Foundation;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Controls.Platform;
 
#if __MOBILE__
using ObjCRuntime;
using UIKit;
using NativeLabel = UIKit.UILabel;
#else
using AppKit;
using NativeLabel = AppKit.NSTextField;
#endif
 
#if __MOBILE__
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
#else
namespace Microsoft.Maui.Controls.Compatibility.Platform.MacOS
#endif
{
	[System.Obsolete(Compatibility.Hosting.MauiAppBuilderExtensions.UseMapperInstead)]
	public class LabelRenderer : ViewRenderer<Label, NativeLabel>
	{
		SizeRequest _perfectSize;
 
		bool _perfectSizeValid;
 
		FormattedString _formatted;
 
		bool IsTextFormatted => _formatted != null;
 
		static HashSet<string> s_perfectSizeSet = new HashSet<string>
		{
			Label.TextProperty.PropertyName,
			Label.TextColorProperty.PropertyName,
			Label.FontAttributesProperty.PropertyName,
			Label.FontFamilyProperty.PropertyName,
			Label.FontSizeProperty.PropertyName,
			Label.FormattedTextProperty.PropertyName,
			Label.LineBreakModeProperty.PropertyName,
			Label.LineHeightProperty.PropertyName,
			Label.PaddingProperty.PropertyName,
			Label.TextTypeProperty.PropertyName
		};
 
		[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
		public LabelRenderer()
		{
 
		}
 
		public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
		{
			if (!_perfectSizeValid)
			{
				_perfectSize = base.GetDesiredSize(double.PositiveInfinity, double.PositiveInfinity);
				_perfectSize.Minimum = new Size(Math.Min(10, _perfectSize.Request.Width), _perfectSize.Request.Height);
				_perfectSizeValid = true;
			}
 
			var widthFits = widthConstraint >= _perfectSize.Request.Width;
			var heightFits = heightConstraint >= _perfectSize.Request.Height;
 
			if (widthFits && heightFits)
				return _perfectSize;
 
			var result = base.GetDesiredSize(widthConstraint, heightConstraint);
			var tinyWidth = Math.Min(10, result.Request.Width);
			result.Minimum = new Size(tinyWidth, result.Request.Height);
 
			if (widthFits || Element.LineBreakMode == LineBreakMode.NoWrap)
				return result;
 
			bool containerIsNotInfinitelyWide = !double.IsInfinity(widthConstraint);
 
			if (containerIsNotInfinitelyWide)
			{
				bool textCouldHaveWrapped = Element.LineBreakMode == LineBreakMode.WordWrap || Element.LineBreakMode == LineBreakMode.CharacterWrap;
				bool textExceedsContainer = result.Request.Width > widthConstraint;
 
				if (textExceedsContainer || textCouldHaveWrapped)
				{
					var expandedWidth = Math.Max(tinyWidth, widthConstraint);
					result.Request = new Size(expandedWidth, result.Request.Height);
				}
			}
 
			return result;
		}
 
		[PortHandler]
#if __MOBILE__
		public override void LayoutSubviews()
		{
			base.LayoutSubviews();
#else
		public override void Layout()
		{
			base.Layout();
#endif
 
			if (Control == null)
				return;
 
			SizeF fitSize;
			nfloat labelHeight;
			switch (Element.VerticalTextAlignment)
			{
				case TextAlignment.Start:
					fitSize = Control.SizeThatFits(Element.Bounds.Size.ToSizeF());
					labelHeight = (nfloat)Math.Min(Bounds.Height, fitSize.Height);
					Control.Frame = new RectangleF(0, 0, (nfloat)Element.Width, labelHeight);
					break;
				case TextAlignment.Center:
 
#if __MOBILE__
					Control.Frame = new RectangleF(0, 0, (nfloat)Element.Width, (nfloat)Element.Height);
#else
					fitSize = Control.SizeThatFits(Element.Bounds.Size.ToSizeF());
					labelHeight = (nfloat)Math.Min(Bounds.Height, fitSize.Height);
					var yOffset = (int)(Element.Height / 2 - labelHeight / 2);
					Control.Frame = new RectangleF(0, 0, (nfloat)Element.Width, (nfloat)Element.Height - yOffset);
#endif
					break;
				case TextAlignment.End:
					fitSize = Control.SizeThatFits(Element.Bounds.Size.ToSizeF());
					labelHeight = (nfloat)Math.Min(Bounds.Height, fitSize.Height);
#if __MOBILE__
					nfloat yOffset = 0;
					yOffset = (nfloat)(Element.Height - labelHeight);
					Control.Frame = new RectangleF(0, yOffset, (nfloat)Element.Width, labelHeight);
#else
					Control.Frame = new RectangleF(0, 0, (nfloat)Element.Width, labelHeight);
#endif
					break;
			}
 
			Control.RecalculateSpanPositions(Element);
 
		}
 
		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);
			if (disposing)
			{
				if (Element != null)
				{
					Element.PropertyChanging -= ElementPropertyChanging;
				}
			}
		}
 
		protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
		{
			_perfectSizeValid = false;
 
			if (e.OldElement != null)
			{
				e.OldElement.PropertyChanging -= ElementPropertyChanging;
			}
 
			if (e.NewElement != null)
			{
				e.NewElement.PropertyChanging += ElementPropertyChanging;
				if (Control == null)
				{
					SetNativeControl(CreateNativeControl());
#if !__MOBILE__
					Control.Editable = false;
					Control.Bezeled = false;
					Control.DrawsBackground = false;
#endif
				}
 
				UpdateLineBreakMode();
				UpdateText();
				UpdateTextDecorations();
				UpdateTextColor();
				UpdateFont();
				UpdateMaxLines();
				UpdateCharacterSpacing();
				UpdatePadding();
				UpdateHorizontalTextAlignment();
			}
 
			base.OnElementChanged(e);
		}
 
		protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			base.OnElementPropertyChanged(sender, e);
 
			if (e.PropertyName == Label.HorizontalTextAlignmentProperty.PropertyName)
				UpdateHorizontalTextAlignment();
			else if (e.PropertyName == Label.VerticalTextAlignmentProperty.PropertyName)
				UpdateLayout();
			else if (e.PropertyName == Label.TextColorProperty.PropertyName)
				UpdateTextColor();
			else if (e.PropertyName == Label.FontAttributesProperty.PropertyName || e.PropertyName == Label.FontFamilyProperty.PropertyName || e.PropertyName == Label.FontSizeProperty.PropertyName)
			{
				UpdateText();
				UpdateTextDecorations();
				UpdateCharacterSpacing();
			}
			else if (e.PropertyName == Label.TextProperty.PropertyName)
			{
				UpdateText();
				UpdateTextDecorations();
				UpdateCharacterSpacing();
			}
			else if (e.PropertyName == Label.CharacterSpacingProperty.PropertyName)
				UpdateCharacterSpacing();
			else if (e.PropertyName == Label.TextDecorationsProperty.PropertyName)
				UpdateTextDecorations();
			else if (e.PropertyName == Label.FormattedTextProperty.PropertyName)
			{
				UpdateText();
				UpdateTextDecorations();
			}
			else if (e.PropertyName == Label.LineBreakModeProperty.PropertyName)
				UpdateLineBreakMode();
			else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
				UpdateHorizontalTextAlignment();
			else if (e.PropertyName == Label.LineHeightProperty.PropertyName)
				UpdateText();
			else if (e.PropertyName == Label.MaxLinesProperty.PropertyName)
				UpdateMaxLines();
			else if (e.PropertyName == Label.PaddingProperty.PropertyName)
				UpdatePadding();
			else if (e.PropertyName == Label.TextTypeProperty.PropertyName)
				UpdateText();
			else if (e.PropertyName == Label.TextTransformProperty.PropertyName)
				UpdateText();
		}
 
		protected override NativeLabel CreateNativeControl()
		{
#if __MOBILE__
			return Element.Padding.IsEmpty ? new NativeLabel(RectangleF.Empty) : new FormsLabel(RectangleF.Empty);
#else
			return new NativeLabel(RectangleF.Empty);
#endif
		}
 
		void ElementPropertyChanging(object sender, PropertyChangingEventArgs e)
		{
			if (s_perfectSizeSet.Contains(e.PropertyName))
				_perfectSizeValid = false;
		}
 
		[PortHandler]
		void UpdateTextDecorations()
		{
			if (IsElementOrControlEmpty)
				return;
 
			if (Element?.TextType != TextType.Text)
				return;
 
#if __MOBILE__
			if (!(Control.AttributedText?.Length > 0))
				return;
#else
			if (!(Control.AttributedStringValue?.Length > 0))
				return;
#endif
 
			var textDecorations = Element.TextDecorations;
#if __MOBILE__
			var newAttributedText = new NSMutableAttributedString(Control.AttributedText);
			var strikeThroughStyleKey = UIStringAttributeKey.StrikethroughStyle;
			var underlineStyleKey = UIStringAttributeKey.UnderlineStyle;
 
#else
			var newAttributedText = new NSMutableAttributedString(Control.AttributedStringValue);
			var strikeThroughStyleKey = NSStringAttributeKey.StrikethroughStyle;
			var underlineStyleKey = NSStringAttributeKey.UnderlineStyle;
#endif
			var range = new NSRange(0, newAttributedText.Length);
 
			if ((textDecorations & TextDecorations.Strikethrough) == 0)
				newAttributedText.RemoveAttribute(strikeThroughStyleKey, range);
			else
				newAttributedText.AddAttribute(strikeThroughStyleKey, NSNumber.FromInt32((int)NSUnderlineStyle.Single), range);
 
			if ((textDecorations & TextDecorations.Underline) == 0)
				newAttributedText.RemoveAttribute(underlineStyleKey, range);
			else
				newAttributedText.AddAttribute(underlineStyleKey, NSNumber.FromInt32((int)NSUnderlineStyle.Single), range);
 
#if __MOBILE__
			Control.AttributedText = newAttributedText;
#else
			Control.AttributedStringValue = newAttributedText;
#endif
			UpdateCharacterSpacing();
			_perfectSizeValid = false;
		}
 
#if __MOBILE__
		protected override void SetAccessibilityLabel()
		{
			// If we have not specified an AccessibilityLabel and the AccessibiltyLabel is current bound to the Text,
			// exit this method so we don't set the AccessibilityLabel value and break the binding.
			// This may pose a problem for users who want to explicitly set the AccessibilityLabel to null, but this
			// will prevent us from inadvertently breaking UI Tests that are using Query.Marked to get the dynamic Text 
			// of the Label.
 
			var elemValue = (string)Element?.GetValue(AutomationProperties.NameProperty);
			if (string.IsNullOrWhiteSpace(elemValue) && Control?.AccessibilityLabel == Control?.Text)
				return;
 
			base.SetAccessibilityLabel();
		}
#endif
 
		protected override void SetBackgroundColor(Color color)
		{
#if __MOBILE__
			if (color == null)
				BackgroundColor = UIColor.Clear;
			else
				BackgroundColor = color.ToPlatform();
#else
			if (color == null)
				Layer.BackgroundColor = NSColor.Clear.CGColor;
			else
				Layer.BackgroundColor = color.ToCGColor();
#endif
		}
 
		protected override void SetBackground(Brush brush)
		{
			var backgroundLayer = this.GetBackgroundLayer(brush);
 
			if (backgroundLayer != null)
			{
#if __MOBILE__
				Layer.BackgroundColor = UIColor.Clear.CGColor;
#endif
				Layer.InsertBackgroundLayer(backgroundLayer, 0);
			}
			else
				Layer.RemoveBackgroundLayer();
		}
 
		[PortHandler]
		void UpdateHorizontalTextAlignment()
		{
#if __MOBILE__
			Control.TextAlignment = Element.HorizontalTextAlignment.ToPlatformTextAlignment(((IVisualElementController)Element).EffectiveFlowDirection);
#else
			Control.Alignment = Element.HorizontalTextAlignment.ToPlatformTextAlignment(((IVisualElementController)Element).EffectiveFlowDirection);
#endif
		}
 
		[PortHandler]
		void UpdateLineBreakMode()
		{
#if __MOBILE__
			switch (Element.LineBreakMode)
			{
				case LineBreakMode.NoWrap:
					Control.LineBreakMode = UILineBreakMode.Clip;
					break;
				case LineBreakMode.WordWrap:
					Control.LineBreakMode = UILineBreakMode.WordWrap;
					break;
				case LineBreakMode.CharacterWrap:
					Control.LineBreakMode = UILineBreakMode.CharacterWrap;
					break;
				case LineBreakMode.HeadTruncation:
					Control.LineBreakMode = UILineBreakMode.HeadTruncation;
					break;
				case LineBreakMode.MiddleTruncation:
					Control.LineBreakMode = UILineBreakMode.MiddleTruncation;
					break;
				case LineBreakMode.TailTruncation:
					Control.LineBreakMode = UILineBreakMode.TailTruncation;
					break;
			}
#else
			switch (Element.LineBreakMode)
			{
				case LineBreakMode.NoWrap:
					Control.LineBreakMode = NSLineBreakMode.Clipping;
					break;
				case LineBreakMode.WordWrap:
					Control.LineBreakMode = NSLineBreakMode.ByWordWrapping;
					break;
				case LineBreakMode.CharacterWrap:
					Control.LineBreakMode = NSLineBreakMode.CharWrapping;
					break;
				case LineBreakMode.HeadTruncation:
					Control.LineBreakMode = NSLineBreakMode.TruncatingHead;
					break;
				case LineBreakMode.MiddleTruncation:
					Control.LineBreakMode = NSLineBreakMode.TruncatingMiddle;
					break;
				case LineBreakMode.TailTruncation:
					Control.LineBreakMode = NSLineBreakMode.TruncatingTail;
					break;
			}
#endif
		}
 
		[PortHandler]
		void UpdateCharacterSpacing()
		{
			if (IsElementOrControlEmpty)
				return;
 
			if (Element?.TextType != TextType.Text)
				return;
 
			if (string.IsNullOrEmpty(Element.Text))
				return;
#if __MOBILE__
			var textAttr = Control.AttributedText.WithCharacterSpacing(Element.CharacterSpacing);
 
			if (textAttr != null)
				Control.AttributedText = textAttr;
#else
			var textAttr = Control.AttributedStringValue.AddCharacterSpacing(Element.Text, Element.CharacterSpacing);
 
			if (textAttr != null)
				Control.AttributedStringValue = textAttr;
#endif
 
			_perfectSizeValid = false;
		}
 
		[PortHandler("Partially. Mapped LineHeight")]
		void UpdateText()
		{
			if (IsElementOrControlEmpty)
				return;
 
			switch (Element.TextType)
			{
				case TextType.Html:
					UpdateTextHtml();
					break;
 
				default:
					UpdateTextPlainText();
					break;
			}
		}
 
		[PortHandler("Partially ported")]
		void UpdateTextPlainText()
		{
			_formatted = Element.FormattedText;
			if (_formatted == null && Element.LineHeight >= 0)
				_formatted = Element.Text;
 
			if (IsTextFormatted)
			{
				UpdateFormattedText();
			}
			else
			{
				var text = Element.UpdateFormsText(Element.Text, Element.TextTransform);
#if __MOBILE__
				Control.Text = text;
#else
				Control.StringValue = text ?? "";
#endif
			}
			UpdateLayout();
		}
 
		[PortHandler("Partially ported")]
		void UpdateFormattedText()
		{
#if __MOBILE__
			Control.AttributedText = _formatted.ToNSAttributedString(Element.RequireFontManager());
#else
			Control.AttributedStringValue = _formatted.ToNSAttributedString(Element.RequireFontManager(), Element.TextColor, Element.HorizontalTextAlignment, Element.LineHeight);
#endif
			_perfectSizeValid = false;
 
			UpdateHorizontalTextAlignment();
		}
 
		void UpdateTextHtml()
		{
			if (IsElementOrControlEmpty)
				return;
 
			string text = Element.Text ?? string.Empty;
 
			var attr = GetNSAttributedStringDocumentAttributes();
#if __MOBILE__
 
			NSError nsError = null;
 
			Control.AttributedText = new NSAttributedString(text, attr, ref nsError);
#else
			var htmlData = new NSMutableData();
			htmlData.SetData(text);
 
			Control.AttributedStringValue = new NSAttributedString(htmlData, attr, out _);
#endif
			_perfectSizeValid = false;
 
			// Setting AttributedText will reset style-related properties, so we'll need to update them again
			UpdateTextColor();
			UpdateFont();
		}
 
		protected virtual NSAttributedStringDocumentAttributes GetNSAttributedStringDocumentAttributes()
		{
			return new NSAttributedStringDocumentAttributes
			{
				DocumentType = NSDocumentType.HTML,
				StringEncoding = NSStringEncoding.UTF8
			};
		}
 
		static bool FontIsDefault(Label label)
		{
			if (label.IsSet(Label.FontAttributesProperty))
			{
				return false;
			}
 
			if (label.IsSet(Label.FontFamilyProperty))
			{
				return false;
			}
 
			if (label.IsSet(Label.FontSizeProperty))
			{
				return false;
			}
 
			return true;
		}
 
		[PortHandler]
		void UpdateFont()
		{
			if (Element == null)
			{
				return;
			}
 
			if (IsTextFormatted)
			{
				UpdateFormattedText();
				return;
			}
 
			if (Element.TextType == TextType.Html && FontIsDefault(Element))
			{
				// If no explicit font properties have been specified and we're display HTML,
				// let the HTML determine the typeface
				return;
			}
 
#if __MOBILE__
			Control.Font = Element.ToUIFont();
#else
			Control.Font = Element.ToNSFont();
#endif
			UpdateLayout();
		}
 
		[PortHandler]
		void UpdateTextColor()
		{
			if (IsTextFormatted)
			{
				UpdateFormattedText();
				return;
			}
 
			var textColor = (Color)Element.GetValue(Label.TextColorProperty);
 
			if (textColor == null && Element.TextType == TextType.Html)
			{
				// If no explicit text color has been specified and we're displaying HTML, 
				// let the HTML determine the colors
				return;
			}
 
#if __MOBILE__
			Control.TextColor = textColor.ToPlatform(Maui.Platform.ColorExtensions.LabelColor);
#else
			var alignment = Element.HorizontalTextAlignment.ToPlatformTextAlignment(((IVisualElementController)Element).EffectiveFlowDirection);
			var textWithColor = new NSAttributedString(Element.Text ?? "", font: Element.ToNSFont(), foregroundColor: textColor.ToNSColor(ColorExtensions.TextColor), paragraphStyle: new NSMutableParagraphStyle() { Alignment = alignment });
			textWithColor = textWithColor.AddCharacterSpacing(Element.Text ?? string.Empty, Element.CharacterSpacing);
			Control.AttributedStringValue = textWithColor;
#endif
			UpdateLayout();
		}
 
		void UpdateLayout()
		{
#if __MOBILE__
			LayoutSubviews();
#else
			Layout();
#endif
		}
 
		[PortHandler("Partially ported")]
		void UpdateMaxLines()
		{
			if (Element.MaxLines >= 0)
			{
#if __MOBILE__
				Control.Lines = Element.MaxLines;
 
				LayoutSubviews();
#else
				Control.MaximumNumberOfLines = Element.MaxLines;
 
				Layout();
#endif
			}
			else
			{
#if __MOBILE__
				switch (Element.LineBreakMode)
				{
					case LineBreakMode.WordWrap:
					case LineBreakMode.CharacterWrap:
						Control.Lines = 0;
						break;
					case LineBreakMode.NoWrap:
					case LineBreakMode.HeadTruncation:
					case LineBreakMode.MiddleTruncation:
					case LineBreakMode.TailTruncation:
						Control.Lines = 1;
						break;
				}
 
				LayoutSubviews();
#else
				switch (Element.LineBreakMode)
				{
					case LineBreakMode.WordWrap:
					case LineBreakMode.CharacterWrap:
						Control.MaximumNumberOfLines = 0;
						break;
					case LineBreakMode.NoWrap:
					case LineBreakMode.HeadTruncation:
					case LineBreakMode.MiddleTruncation:
					case LineBreakMode.TailTruncation:
						Control.MaximumNumberOfLines = 1;
						break;
				}
 
				Layout();
#endif
			}
		}
 
		void UpdatePadding()
		{
			if (IsElementOrControlEmpty)
				return;
 
			if (Element.Padding.IsEmpty)
				return;
 
#if __MOBILE__
			var formsLabel = Control as FormsLabel;
			if (formsLabel == null)
			{
				Debug.WriteLine($"{nameof(LabelRenderer)}: On iOS, a Label created with no padding will ignore padding changes");
				return;
			}
 
			formsLabel.TextInsets = new UIEdgeInsets(
					(float)Element.Padding.Top,
					(float)Element.Padding.Left,
					(float)Element.Padding.Bottom,
					(float)Element.Padding.Right);
			UpdateLayout();
#endif
		}
 
#if __MOBILE__
		class FormsLabel : NativeLabel
		{
			public UIEdgeInsets TextInsets { get; set; }
 
			public FormsLabel(RectangleF frame) : base(frame)
			{
			}
 
			public override void DrawText(RectangleF rect) => base.DrawText(TextInsets.InsetRect(rect));
 
			public override SizeF SizeThatFits(SizeF size) => AddInsets(base.SizeThatFits(size));
 
			SizeF AddInsets(SizeF size) => new SizeF(
				width: size.Width + TextInsets.Left + TextInsets.Right,
				height: size.Height + TextInsets.Top + TextInsets.Bottom);
		}
#endif
	}
}