File: iOS\Renderers\ButtonLayoutManager.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using CoreGraphics;
using Foundation;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
	// TODO: The entire layout system. iOS buttons were not designed for
	//       anything but image left, text right, single line layouts.
 
	[Obsolete]
	public class ButtonLayoutManager : IDisposable
	{
		bool _disposed;
		IButtonLayoutRenderer _renderer;
		Button _element;
		bool _preserveInitialPadding;
		bool _spacingAdjustsPadding;
		bool _borderAdjustsPadding;
		bool _collapseHorizontalPadding;
 
		UIEdgeInsets? _defaultImageInsets;
		UIEdgeInsets? _defaultTitleInsets;
		UIEdgeInsets? _defaultContentInsets;
 
		UIEdgeInsets _paddingAdjustments = new UIEdgeInsets();
 
		public ButtonLayoutManager(IButtonLayoutRenderer renderer,
			bool preserveInitialPadding = false,
			bool spacingAdjustsPadding = true,
			bool borderAdjustsPadding = false,
			bool collapseHorizontalPadding = false)
		{
			_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
			_renderer.ElementChanged += OnElementChanged;
			_preserveInitialPadding = preserveInitialPadding;
			_spacingAdjustsPadding = spacingAdjustsPadding;
			_borderAdjustsPadding = borderAdjustsPadding;
			_collapseHorizontalPadding = collapseHorizontalPadding;
 
			ImageElementManager.Init(renderer.ImageVisualElementRenderer);
		}
 
		UIButton Control => _renderer?.Control;
 
		IImageVisualElementRenderer ImageVisualElementRenderer => _renderer?.ImageVisualElementRenderer;
 
		public void Dispose()
		{
			Dispose(true);
		}
 
		protected virtual void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					if (_renderer != null)
					{
						var imageRenderer = ImageVisualElementRenderer;
						if (imageRenderer != null)
							ImageElementManager.Dispose(imageRenderer);
 
						_renderer.ElementChanged -= OnElementChanged;
						_renderer = null;
					}
				}
				_disposed = true;
			}
		}
 
		public CGSize SizeThatFits(CGSize size, CGSize measured)
		{
			if (_disposed || _renderer == null || _element == null)
				return measured;
 
			var control = Control;
			if (control == null)
				return measured;
 
			var minHeight = _renderer.MinimumHeight;
			if (measured.Height < minHeight)
				measured.Height = minHeight;
 
			var titleHeight = control.TitleLabel.Frame.Height;
			if (titleHeight > measured.Height)
				measured.Height = titleHeight;
 
			return measured;
		}
 
		public void Update()
		{
			UpdatePadding();
			_ = UpdateImageAsync();
			UpdateText();
			UpdateEdgeInsets();
			UpdateLineBreakMode();
		}
 
		[PortHandler]
		void UpdateLineBreakMode()
		{
			var control = Control;
 
			if (_disposed || _renderer == null || _element == null || control == null)
				return;
 
			control.TitleLabel.LineBreakMode = _element.LineBreakMode switch
			{
				LineBreakMode.NoWrap => UILineBreakMode.Clip,
				LineBreakMode.WordWrap => UILineBreakMode.WordWrap,
				LineBreakMode.CharacterWrap => UILineBreakMode.CharacterWrap,
				LineBreakMode.HeadTruncation => UILineBreakMode.HeadTruncation,
				LineBreakMode.TailTruncation => UILineBreakMode.TailTruncation,
				LineBreakMode.MiddleTruncation => UILineBreakMode.MiddleTruncation,
				_ => throw new ArgumentOutOfRangeException()
			};
		}
 
		public void SetImage(UIImage image)
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var control = Control;
			if (control == null)
				return;
 
			if (image != null)
			{
				control.SetImage(image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal), UIControlState.Normal);
				control.ImageView.ContentMode = UIViewContentMode.ScaleAspectFit;
			}
			else
			{
				control.SetImage(null, UIControlState.Normal);
			}
 
			UpdateEdgeInsets();
		}
 
		void OnElementChanged(object sender, ElementChangedEventArgs<Button> e)
		{
			if (_element != null)
			{
				_element.PropertyChanged -= OnElementPropertyChanged;
				_element = null;
			}
 
			if (e.NewElement is Button button)
			{
				_element = button;
				_element.PropertyChanged += OnElementPropertyChanged;
			}
 
			Update();
		}
 
		void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			if (e.PropertyName == Button.PaddingProperty.PropertyName)
				UpdatePadding();
			else if (e.PropertyName == Button.ImageSourceProperty.PropertyName)
				_ = UpdateImageAsync();
			else if (e.PropertyName == Button.TextProperty.PropertyName ||
					 e.PropertyName == Button.TextTransformProperty.PropertyName ||
					 e.PropertyName == Button.CharacterSpacingProperty.PropertyName)
				UpdateText();
			else if (e.PropertyName == Button.ContentLayoutProperty.PropertyName)
				UpdateEdgeInsets();
			else if (e.PropertyName == Button.BorderWidthProperty.PropertyName && _borderAdjustsPadding)
				UpdateEdgeInsets();
			else if (e.PropertyName == Button.LineBreakModeProperty.PropertyName)
				UpdateLineBreakMode();
		}
 
		internal void UpdateText()
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var control = Control;
			if (control == null)
				return;
 
			var transformedText = _element.UpdateFormsText(_element.Text, _element.TextTransform);
 
			control.SetTitle(transformedText, UIControlState.Normal);
 
			var normalTitle = control
				.GetAttributedTitle(UIControlState.Normal);
 
			if (_element.CharacterSpacing == 0 && normalTitle == null)
			{
				control.SetTitle(transformedText, UIControlState.Normal);
				return;
			}
 
			if (control.Title(UIControlState.Normal) != null)
				control.SetTitle(null, UIControlState.Normal);
 
			string text = transformedText ?? string.Empty;
			var colorRange = new NSRange(0, text.Length);
 
			var normal =
				control
					.GetAttributedTitle(UIControlState.Normal)
					.WithCharacterSpacing(_element.CharacterSpacing);
 
			var highlighted =
				control
					.GetAttributedTitle(UIControlState.Highlighted)
					.WithCharacterSpacing(_element.CharacterSpacing);
 
			var disabled =
				control
					.GetAttributedTitle(UIControlState.Disabled)
					.WithCharacterSpacing(_element.CharacterSpacing);
 
			normal.AddAttribute(
				UIStringAttributeKey.ForegroundColor,
				Control.TitleColor(UIControlState.Normal),
				colorRange);
 
			highlighted.AddAttribute(
				UIStringAttributeKey.ForegroundColor,
				Control.TitleColor(UIControlState.Highlighted),
				colorRange);
 
			disabled.AddAttribute(
				UIStringAttributeKey.ForegroundColor,
				Control.TitleColor(UIControlState.Disabled),
				colorRange);
 
			Control.SetAttributedTitle(normal, UIControlState.Normal);
			Control.SetAttributedTitle(highlighted, UIControlState.Highlighted);
			Control.SetAttributedTitle(disabled, UIControlState.Disabled);
 
			UpdateEdgeInsets();
		}
 
		async Task UpdateImageAsync()
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var imageRenderer = ImageVisualElementRenderer;
			if (imageRenderer == null)
				return;
 
			try
			{
				await ImageElementManager.SetImage(imageRenderer, _element);
			}
			catch (Exception ex)
			{
				Forms.MauiContext?.CreateLogger<ButtonLayoutManager>()?.LogWarning(ex, "Error loading image");
			}
		}
 
#pragma warning disable CA1416, CA1422  // TOD0: UIButton.ContentEdgeInsets, UIButton.ImageEdgeInsets is unsupported on: 'ios' 15.0 and later
		[PortHandler]
		void UpdatePadding()
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var control = Control;
			if (control == null)
				return;
 
			EnsureDefaultInsets();
 
			control.ContentEdgeInsets = GetPaddingInsets(_paddingAdjustments);
		}
 
		UIEdgeInsets GetPaddingInsets(UIEdgeInsets adjustments = default(UIEdgeInsets))
		{
			var defaultPadding = _preserveInitialPadding && _defaultContentInsets.HasValue
				? _defaultContentInsets.Value
				: new UIEdgeInsets();
 
			return new UIEdgeInsets(
				(nfloat)_element.Padding.Top + defaultPadding.Top + adjustments.Top,
				(nfloat)_element.Padding.Left + defaultPadding.Left + adjustments.Left,
				(nfloat)_element.Padding.Bottom + defaultPadding.Bottom + adjustments.Bottom,
				(nfloat)_element.Padding.Right + defaultPadding.Right + adjustments.Right);
		}
 
		void EnsureDefaultInsets()
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var control = Control;
			if (control == null)
				return;
 
			if (_defaultImageInsets == null)
				_defaultImageInsets = control.ImageEdgeInsets;
			if (_defaultTitleInsets == null)
				_defaultTitleInsets = control.TitleEdgeInsets;
			if (_defaultContentInsets == null)
				_defaultContentInsets = control.ContentEdgeInsets;
		}
 
		void UpdateEdgeInsets()
		{
			if (_disposed || _renderer == null || _element == null)
				return;
 
			var control = Control;
			if (control == null)
				return;
 
			EnsureDefaultInsets();
 
			_paddingAdjustments = new UIEdgeInsets();
 
			var imageInsets = new UIEdgeInsets();
			var titleInsets = new UIEdgeInsets();
 
			// adjust for the border
			if (_borderAdjustsPadding && _element is IBorderElement borderElement && borderElement.IsBorderWidthSet() && borderElement.BorderWidth != borderElement.BorderWidthDefaultValue)
			{
				var width = (nfloat)_element.BorderWidth;
				_paddingAdjustments.Top += width;
				_paddingAdjustments.Bottom += width;
				_paddingAdjustments.Left += width;
				_paddingAdjustments.Right += width;
			}
 
			var layout = _element.ContentLayout;
 
			var spacing = (nfloat)layout.Spacing;
			var halfSpacing = spacing / 2;
 
			var image = control.CurrentImage;
			if (image != null && !string.IsNullOrEmpty(control.CurrentTitle))
			{
				// TODO: Do not use the title label as it is not yet updated and
				//       if we move the image, then we technically have more
				//       space and will require a new layout pass.
 
				var title =
					control.CurrentAttributedTitle ??
					new NSAttributedString(control.CurrentTitle, new UIStringAttributes { Font = control.TitleLabel.Font });
				var titleRect = title.GetBoundingRect(
					control.Bounds.Size,
					NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading,
					null);
 
				var titleWidth = titleRect.Width;
				var titleHeight = titleRect.Height;
				var imageWidth = image.Size.Width;
				var imageHeight = image.Size.Height;
 
				// adjust the padding for the spacing
				if (layout.IsHorizontal())
				{
					var adjustment = _spacingAdjustsPadding ? halfSpacing * 2 : halfSpacing;
					_paddingAdjustments.Left += adjustment;
					_paddingAdjustments.Right += adjustment;
				}
				else
				{
					var adjustment = _spacingAdjustsPadding ? halfSpacing * 2 : halfSpacing;
 
					_paddingAdjustments.Top += adjustment;
					_paddingAdjustments.Bottom += adjustment;
				}
 
				// move the images according to the layout
				if (layout.Position == Button.ButtonContentLayout.ImagePosition.Left)
				{
					// add a bit of spacing
					imageInsets.Left -= halfSpacing;
					imageInsets.Right += halfSpacing;
					titleInsets.Left += halfSpacing;
					titleInsets.Right -= halfSpacing;
				}
				else if (layout.Position == Button.ButtonContentLayout.ImagePosition.Right)
				{
					// swap the elements and add spacing
					imageInsets.Left += titleWidth + halfSpacing;
					imageInsets.Right -= titleWidth + halfSpacing;
					titleInsets.Left -= imageWidth + halfSpacing;
					titleInsets.Right += imageWidth + halfSpacing;
				}
				else
				{
					// we will move the image and title vertically
					var imageVertical = (titleHeight / 2) + halfSpacing;
					var titleVertical = (imageHeight / 2) + halfSpacing;
 
					// the width will be different now that the image is no longer next to the text
					nfloat horizontalAdjustment = 0;
					if (_collapseHorizontalPadding)
						horizontalAdjustment = (nfloat)(titleWidth + imageWidth - Math.Max(titleWidth, imageWidth)) / 2;
					_paddingAdjustments.Left -= horizontalAdjustment;
					_paddingAdjustments.Right -= horizontalAdjustment;
 
					// the height will also be different
					var verticalAdjustment = (nfloat)Math.Min(imageVertical, titleVertical);
					_paddingAdjustments.Top += verticalAdjustment;
					_paddingAdjustments.Bottom += verticalAdjustment;
 
					// if the image is at the bottom, swap the direction
					if (layout.Position == Button.ButtonContentLayout.ImagePosition.Bottom)
					{
						imageVertical = -imageVertical;
						titleVertical = -titleVertical;
					}
 
					// move the image and title vertically
					imageInsets.Top -= imageVertical;
					imageInsets.Bottom += imageVertical;
					titleInsets.Top += titleVertical;
					titleInsets.Bottom -= titleVertical;
 
					// center the elements horizontally
					var imageHorizontal = titleWidth / 2;
					var titleHorizontal = imageWidth / 2;
					imageInsets.Left += imageHorizontal;
					imageInsets.Right -= imageHorizontal;
					titleInsets.Left -= titleHorizontal;
					titleInsets.Right += titleHorizontal;
				}
			}
 
			UpdatePadding();
			control.ImageEdgeInsets = imageInsets;
			control.TitleEdgeInsets = titleInsets;
		}
#pragma warning restore CA1416, CA1422  // UIButton.ContentEdgeInsets, UIButton.ImageEdgeInsets is unsupported on: 'ios' 15.0 and later
	}
}