File: ViewExtensions.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls
{
	/// <summary>
	/// Extension methods for <see cref="VisualElement" />s, providing animatable scaling, rotation, and layout functions.
	/// </summary>
	public static class ViewExtensions
	{
		/// <summary>
		/// Aborts all animations (e.g. <c>LayoutTo</c>, <c>TranslateTo</c>, <c>ScaleTo</c>, etc.) on the <paramref name= "view" /> element.
		/// </summary>
		///	<param name="view">The view on which this method operates.</param>
		/// <exception cref="ArgumentNullException">Thrown if <paramref name="view"/> is <see langword="null"/>.</exception>
		public static void CancelAnimations(this VisualElement view)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			view.AbortAnimation(nameof(LayoutTo));
			view.AbortAnimation(nameof(TranslateTo));
			view.AbortAnimation(nameof(RotateTo));
			view.AbortAnimation(nameof(RotateYTo));
			view.AbortAnimation(nameof(RotateXTo));
			view.AbortAnimation(nameof(ScaleTo));
			view.AbortAnimation(nameof(ScaleXTo));
			view.AbortAnimation(nameof(ScaleYTo));
			view.AbortAnimation(nameof(FadeTo));
		}
 
		static Task<bool> AnimateTo(this VisualElement view, double start, double end, string name,
			Action<VisualElement, double> updateAction, uint length = 250, Easing? easing = null)
		{
			if (easing == null)
				easing = Easing.Linear;
 
			var tcs = new TaskCompletionSource<bool>();
 
			var weakView = new WeakReference<VisualElement>(view);
 
			void UpdateProperty(double f)
			{
				if (weakView.TryGetTarget(out VisualElement? v))
				{
					updateAction(v, f);
				}
			}
 
			new Animation(UpdateProperty, start, end, easing).Commit(view, name, 16, length, finished: (f, a) => tcs.SetResult(a));
 
			return tcs.Task;
		}
 
 
		/// <summary>
		/// Returns a task that performs the fade that is described by the <paramref name="opacity" />, <paramref name = "length" />, and <paramref name="easing" /> parameters.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="opacity">The opacity to fade to.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> FadeTo(this VisualElement view, double opacity, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.Opacity, opacity, nameof(FadeTo), (v, value) => v.Opacity = value, length, easing);
		}
 
		/// <summary>
		/// <summary>Returns a task that eases the bounds of the <see cref="VisualElement" /> that is specified by the <paramref name="view" />
		/// to the rectangle that is specified by the <paramref name="bounds" /> parameter.</summary>
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="bounds">The layout bounds.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> LayoutTo(this VisualElement view, Rect bounds, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			Rect start = view.Bounds;
			Func<double, Rect> computeBounds = progress =>
			{
				double x = start.X + (bounds.X - start.X) * progress;
				double y = start.Y + (bounds.Y - start.Y) * progress;
				double w = start.Width + (bounds.Width - start.Width) * progress;
				double h = start.Height + (bounds.Height - start.Height) * progress;
 
				return new Rect(x, y, w, h);
			};
 
			return AnimateTo(view, 0, 1, nameof(LayoutTo), (v, value) => v.Layout(computeBounds(value)), length, easing);
		}
 
		/// <summary>
		/// Rotates the <see cref="VisualElement" /> that is specified by <paramref name="view" /> from its current rotation by <paramref name="drotation" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="drotation">The relative rotation.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> RelRotateTo(this VisualElement view, double drotation, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return view.RotateTo(view.Rotation + drotation, length, easing);
		}
 
		/// <summary>
		/// Returns a task that scales the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// from its current scale to <paramref name="dscale" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="dscale">The relative scale.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> RelScaleTo(this VisualElement view, double dscale, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return view.ScaleTo(view.Scale + dscale, length, easing);
		}
 
		/// <summary>
		/// Returns a task that rotates the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// that is described by the <paramref name="rotation" />, <paramref name="length" />, and <paramref name="easing" /> parameters.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="rotation">The final rotation value.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> RotateTo(this VisualElement view, double rotation, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.Rotation, rotation, nameof(RotateTo), (v, value) => v.Rotation = value, length, easing);
		}
 
		/// <summary>
		/// Returns a task that skews the X axis of the the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// by <paramref name="rotation" />, taking time <paramref name="length" /> and using <paramref name="easing" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="rotation">The final rotation value.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> RotateXTo(this VisualElement view, double rotation, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.RotationX, rotation, nameof(RotateXTo), (v, value) => v.RotationX = value, length, easing);
		}
 
		/// <summary>
		/// Returns a task that skews the Y axis of the the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// by <paramref name="rotation" />, taking time <paramref name="length" /> and using <paramref name="easing" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="rotation">The final rotation value.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> RotateYTo(this VisualElement view, double rotation, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.RotationY, rotation, nameof(RotateYTo), (v, value) => v.RotationY = value, length, easing);
		}
 
		/// <summary>
		/// Returns a task that scales the <see cref="VisualElement" /> that is specified by <paramref name="view" /> to the absolute scale factor <paramref name="scale" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="scale">The final absolute scale.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> ScaleTo(this VisualElement view, double scale, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.Scale, scale, nameof(ScaleTo), (v, value) => v.Scale = value, length, easing);
		}
 
		/// <summary>
		/// Returns a task that scales the X axis of the the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// to the absolute scale factor <paramref name="scale" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="scale">The final absolute scale.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> ScaleXTo(this VisualElement view, double scale, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.ScaleX, scale, nameof(ScaleXTo), (v, value) => v.ScaleX = value, length, easing);
		}
 
		/// <summary>
		/// Returns a task that scales the Y axis of the the <see cref="VisualElement" /> that is specified by <paramref name="view" />
		/// to the absolute scale factor <paramref name="scale" />.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="scale">The final absolute scale.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> ScaleYTo(this VisualElement view, double scale, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			return AnimateTo(view, view.ScaleY, scale, nameof(ScaleYTo), (v, value) => v.ScaleY = value, length, easing);
		}
 
		/// <summary>
		/// Animates an elements <see cref="VisualElement.TranslationX"/> and <see cref="VisualElement.TranslationY"/> properties
		/// from their current values to the new values. This ensures that the input layout is in the same position as the visual layout.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		/// <param name="x">The x component of the final translation vector.</param>
		/// <param name="y">The y component of the final translation vector.</param>
		/// <param name="length">The time, in milliseconds, over which to animate the transition. The default is 250.</param>
		/// <param name="easing">The easing function to use for the animation.</param>
		/// <returns>A <see cref="Task"/> containing a <see cref="bool"/> value which indicates whether the animation was canceled. <see langword="true"/> indicates that the animation was canceled. <see langword="false"/> indicates that the animation ran to completion.</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="view"/> is <see langword="null"/>.</exception>
		public static Task<bool> TranslateTo(this VisualElement view, double x, double y, uint length = 250, Easing? easing = null)
		{
			if (view == null)
				throw new ArgumentNullException(nameof(view));
 
			easing ??= Easing.Linear;
 
			var tcs = new TaskCompletionSource<bool>();
			var weakView = new WeakReference<VisualElement>(view);
			Action<double> translateX = f =>
			{
				if (weakView.TryGetTarget(out VisualElement? v))
					v.TranslationX = f;
			};
			Action<double> translateY = f =>
			{
				if (weakView.TryGetTarget(out VisualElement? v))
					v.TranslationY = f;
			};
 
			new Animation
			{
				{ 0, 1, new Animation(translateX, view.TranslationX, x, easing: easing) },
				{ 0, 1, new Animation(translateY, view.TranslationY, y, easing: easing) }
			}.Commit(view, nameof(TranslateTo), 16, length, null, (f, a) => tcs.SetResult(a));
 
			return tcs.Task;
		}
 
		internal static IAnimationManager GetAnimationManager(this IAnimatable animatable)
		{
			if (animatable is Element element)
			{
				if (element.FindMauiContext() is IMauiContext viewMauiContext)
					return viewMauiContext.GetAnimationManager();
 
				if (Application.Current?.FindMauiContext() is IMauiContext applicationMauiContext)
					return applicationMauiContext.GetAnimationManager();
			}
 
			throw new ArgumentException($"Unable to find {nameof(IAnimationManager)} for '{animatable.GetType().FullName}'.", nameof(animatable));
		}
 
		internal static IMauiContext RequireMauiContext(this Element element, bool fallbackToAppMauiContext = false)
			=> element.FindMauiContext(fallbackToAppMauiContext) ?? throw new InvalidOperationException($"{nameof(IMauiContext)} not found.");
 
		internal static IMauiContext? FindMauiContext(this Element element, bool fallbackToAppMauiContext = false)
		{
			if (element is Maui.IElement fe && fe.Handler?.MauiContext != null)
				return fe.Handler.MauiContext;
 
			foreach (var parent in element.GetParentsPath())
			{
				if (parent is Maui.IElement parentView && parentView.Handler?.MauiContext != null)
					return parentView.Handler.MauiContext;
			}
 
			return fallbackToAppMauiContext ? Application.Current?.FindMauiContext() : default;
		}
 
		internal static ILogger<T>? CreateLogger<T>(this Element element, bool fallbackToAppMauiContext = true) =>
			element.FindMauiContext(fallbackToAppMauiContext)?.CreateLogger<T>();
 
		internal static IFontManager RequireFontManager(this Element element, bool fallbackToAppMauiContext = false)
			=> element.RequireMauiContext(fallbackToAppMauiContext).Services.GetRequiredService<IFontManager>();
 
		internal static double GetDefaultFontSize(this Element element)
			=> element.FindMauiContext()?.Services?.GetService<IFontManager>()?.DefaultFontSize ?? 0d;
 
		internal static Element? FindParentWith(this Element element, Func<Element, bool> withMatch, bool includeThis = false)
		{
			if (includeThis && withMatch(element))
				return element;
 
			foreach (var parent in element.GetParentsPath())
			{
				if (withMatch(parent))
					return parent;
			}
 
			return default;
		}
 
		internal static T? FindParentOfType<T>(this Element element, bool includeThis = false)
			where T : Maui.IElement
		{
			if (includeThis && element is T view)
				return view;
 
			foreach (var parent in element.GetParentsPath())
			{
				if (parent is T parentView)
					return parentView;
			}
 
			return default;
		}
 
		internal static IList<IGestureRecognizer>? GetCompositeGestureRecognizers(this Element element)
		{
			if (element is IGestureController gc)
				return gc.CompositeGestureRecognizers;
 
			return null;
		}
 
		internal static IEnumerable<Element> GetParentsPath(this Element self)
		{
			Element current = self;
 
			while (!Application.IsApplicationOrNull(current.RealParent))
			{
				current = current.RealParent;
				yield return current;
			}
		}
 
		internal static List<Page> GetParentPages(this Page target)
		{
			var result = new List<Page>();
 
			var parent = target.RealParent as Page;
			while (!Application.IsApplicationOrWindowOrNull(parent))
			{
				result.Add(parent!);
				parent = parent!.RealParent as Page;
			}
 
			return result;
		}
 
		internal static string? GetStringValue(this IView element)
		{
			string? text = null;
 
			if (element is ILabel label)
				text = label.Text;
			else if (element is IEntry entry)
				text = entry.Text;
			else if (element is IEditor editor)
				text = editor.Text;
			else if (element is ITimePicker tp)
				text = tp.Time.ToString();
			else if (element is IDatePicker dp)
				text = dp.Date.ToString();
			else if (element is ICheckBox cb)
				text = cb.IsChecked.ToString();
			else if (element is ISwitch sw)
				text = sw.IsOn.ToString();
			else if (element is IRadioButton rb)
				text = rb.IsChecked.ToString();
 
			return text;
		}
 
		internal static bool TrySetValue(this Element element, string text)
		{
			if (element is Label label)
			{
				label.Text = text;
				return true;
			}
			else if (element is Entry entry)
			{
				entry.Text = text;
				return true;
			}
			else if (element is Editor editor)
			{
				editor.Text = text;
				return true;
			}
			else if (element is CheckBox cb && bool.TryParse(text, out bool result))
			{
				cb.IsChecked = result;
				return true;
			}
			else if (element is Switch sw && bool.TryParse(text, out bool swResult))
			{
				sw.IsToggled = swResult;
				return true;
			}
			else if (element is RadioButton rb && bool.TryParse(text, out bool rbResult))
			{
				rb.IsChecked = rbResult;
				return true;
			}
			else if (element is TimePicker tp && TimeSpan.TryParse(text, out TimeSpan tpResult))
			{
				tp.Time = tpResult;
				return true;
			}
			else if (element is DatePicker dp && DateTime.TryParse(text, out DateTime dpResult))
			{
				dp.Date = dpResult;
				return true;
			}
 
			return false;
		}
 
		static internal bool RequestFocus(this VisualElement view)
		{
			// if there is an attached handler, we use that and we will end up in the MapFocus method below
			if (view.Handler is IViewHandler handler)
				return handler.InvokeWithResult(nameof(IView.Focus), new FocusRequest());
 
			// if there is no handler, we need to still run some code
			var focusRequest = new FocusRequest();
			view.MapFocus(focusRequest);
			return focusRequest.Result;
		}
 
		static internal void MapFocus(this VisualElement view, FocusRequest focusRequest, Action? baseMethod = null)
		{
			// the virtual view is already focused
			if (view.IsFocused)
			{
				focusRequest.TrySetResult(true);
				return;
			}
 
			// if there are legacy events, then use that
			if (view.HasFocusChangeRequestedEvent)
			{
				var arg = new VisualElement.FocusRequestArgs { Focus = true };
				view.InvokeFocusChangeRequested(arg);
				focusRequest.TrySetResult(arg.Result);
				return;
			}
 
			// otherwise, fall back to "base"
			if (baseMethod is not null)
			{
				baseMethod.Invoke();
				return;
			}
 
			// if there was nothing that handles this, then nothing changed
			focusRequest.TrySetResult(false);
		}
 
		internal static IMauiContext? GetCurrentlyPresentedMauiContext(this Element element)
		{
			var window = (element as Window) ?? (element as IWindowController)?.Window;
 
			if (window is null)
				return null;
 
			var modalStack = window.Navigation.ModalStack;
			if (modalStack.Count > 0)
			{
				var currentPage = modalStack[modalStack.Count - 1];
				if (currentPage.Handler?.MauiContext is IMauiContext mauiContext)
				{
					return mauiContext;
				}
			}
 
			return window.Handler?.MauiContext;
		}
 
		/// <summary>
		/// Layout updates can be forced by app code rather than relying on the built-in layout system behavior. However, that is not generally recommended. 
		/// Calling InvalidateArrange, InvalidateMeasure or UpdateLayout is usually unnecessary and can cause poor performance if overused. 
		/// In many situations where app code might be changing layout properties, the layout system will probably already be processing updates asynchronously. 
		/// The layout system also has optimizations for dealing with cascades of layout changes through parent-child relationships, 
		/// and forcing layout with app code can work against such optimizations. Nevertheless, 
		/// it's possible that layout situations exist in more complicated scenarios where forcing layout is the best option for resolving a timing issue or other issue with layout. 
		/// Just use it deliberately and sparingly.
		/// </summary>
		/// <param name="view">The view on which this method operates.</param>
		public static void InvalidateMeasure(this VisualElement view)
		{
			(view as IView)?.InvalidateMeasure();
		}
	}
}