File: Layout\Layout.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls.Xaml.Diagnostics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
 
namespace Microsoft.Maui.Controls
{
	/// <summary>
	/// Base class for layouts that allow you to arrange and group UI controls in your application.
	/// </summary>
	[ContentProperty(nameof(Children))]
	public abstract partial class Layout : View, Maui.ILayout, IList<IView>, IBindableLayout, IPaddingElement, IVisualTreeElement, ISafeAreaView, IInputTransparentContainerElement
	{
		protected ILayoutManager _layoutManager;
 
		ILayoutManager LayoutManager
		{
			get
			{
				return _layoutManager ??= GetLayoutManagerFromFactory(this) ?? CreateLayoutManager();
			}
		}
 
		static ILayoutManager GetLayoutManagerFromFactory(Layout layout)
		{
			var factory = layout.FindMauiContext()?.Services?.GetService<ILayoutManagerFactory>();
			return factory?.CreateLayoutManager(layout);
		}
 
		// The actual backing store for the IViews in the ILayout
		readonly private protected List<IView> _children = new();
 
		/// <summary>
		/// Gets the child objects contained in this layout.
		/// </summary>
		public IList<IView> Children => this;
 
		IList IBindableLayout.Children => _children;
 
		/// <summary>
		/// Gets the child object count in this layout.
		/// </summary>
		public int Count => _children.Count;
 
		/// <summary>
		/// Gets whether this layout is readonly.
		/// </summary>
		public bool IsReadOnly => ((ICollection<IView>)_children).IsReadOnly;
 
		public IView this[int index]
		{
			get => _children[index];
			set
			{
				var old = _children[index];
 
				if (old == value)
				{
					return;
				}
 
				_children.RemoveAt(index);
				if (old is Element oldElement)
				{
					RemoveLogicalChild(oldElement);
				}
 
				_children.Insert(index, value);
 
				if (value is Element newElement)
				{
					InsertLogicalChild(index, newElement);
				}
 
				OnUpdate(index, value, old);
			}
		}
 
		/// <summary>Bindable property for <see cref="IsClippedToBounds"/>.</summary>
		public static readonly BindableProperty IsClippedToBoundsProperty =
			BindableProperty.Create(nameof(IsClippedToBounds), typeof(bool), typeof(Layout), false,
				propertyChanged: IsClippedToBoundsPropertyChanged);
 
		/// <summary>
		/// Gets or sets a value which determines if the layout should clip its children to its bounds.
		/// The default value is <see langword="false"/>.
		/// </summary>
		public bool IsClippedToBounds
		{
			get => (bool)GetValue(IsClippedToBoundsProperty);
			set => SetValue(IsClippedToBoundsProperty, value);
		}
 
		static void IsClippedToBoundsPropertyChanged(BindableObject bindableObject, object oldValue, object newValue)
		{
			if (bindableObject is IView view)
			{
				view.Handler?.UpdateValue(nameof(Maui.ILayout.ClipsToBounds));
			}
		}
 
		bool Maui.ILayout.ClipsToBounds => IsClippedToBounds;
 
		/// <summary>Bindable property for <see cref="Padding"/>.</summary>
		public static readonly BindableProperty PaddingProperty = PaddingElement.PaddingProperty;
 
		/// <summary>
		/// Gets or sets the inner padding of the layout.
		/// The default value is a <see cref="Thickness"/> with all values set to 0.
		/// </summary>
		/// <remarks>The padding is the space between the bounds of a layout and the bounding region into which its children should be arranged into.</remarks>
		public Thickness Padding
		{
			get => (Thickness)GetValue(PaddingElement.PaddingProperty);
			set => SetValue(PaddingElement.PaddingProperty, value);
		}
 
		/// <inheritdoc cref="ISafeAreaView.IgnoreSafeArea"/>
		public bool IgnoreSafeArea { get; set; }
 
		/// <summary>
		/// Creates a manager object that can measure this layout and arrange its children.
		/// </summary>
		/// <returns>An object that implements <see cref="ILayoutManager"/> that manages this layout.</returns>
		protected abstract ILayoutManager CreateLayoutManager();
 
		/// <summary>
		/// Returns an enumerator that lists all of the children in this layout.
		/// </summary>
		/// <returns>A <see cref="IEnumerator{T}"/> of type <see cref="IView"/> with all the children in this layout.</returns>
		public IEnumerator<IView> GetEnumerator() => _children.GetEnumerator();
 
		IEnumerator IEnumerable.GetEnumerator() => _children.GetEnumerator();
 
		protected override void InvalidateMeasureOverride()
		{
			base.InvalidateMeasureOverride();
		}
 
		/// <summary>
		/// Adds a child view to the end of this layout.
		/// </summary>
		/// <param name="child">The child view to add.</param>
		public void Add(IView child)
		{
			if (child == null)
			{
				return;
			}
 
			var index = _children.Count;
			_children.Add(child);
 
			if (child is Element element)
			{
				AddLogicalChild(element);
			}
 
			OnAdd(index, child);
		}
 
		/// <summary>
		/// Clears all child views from this layout.
		/// </summary>
		public void Clear()
		{
			for (int i = _children.Count - 1; i >= 0; i--)
			{
				var child = _children[i];
				_children.RemoveAt(i);
				if (child is Element element)
				{
					RemoveLogicalChild(element);
				}
			}
			OnClear();
		}
 
		/// <summary>
		/// Determines if the specified child view is contained in this layout.
		/// </summary>
		/// <param name="item">The child view for which to determine if it is contained in this layout.</param>
		/// <returns><see langword="true"/> if <paramref name="item"/> exists in this layout, otherwise <see langword="false"/>.</returns>
		public bool Contains(IView item)
		{
			return _children.Contains(item);
		}
 
		/// <summary>
		/// Copies the child views to the specified array.
		/// </summary>
		/// <param name="array">The target array to copy the child views to.</param>
		/// <param name="arrayIndex">The index at which the copying needs to start.</param>
		public void CopyTo(IView[] array, int arrayIndex)
		{
			_children.CopyTo(array, arrayIndex);
		}
 
		/// <summary>
		/// Gets the index of a specified child view.
		/// </summary>
		/// <param name="item">The child view of which to determine the index.</param>
		/// <returns>The index of the specified view, if the view was not found this will return <c>-1</c>.</returns>
		public int IndexOf(IView item)
		{
			return _children.IndexOf(item);
		}
 
		/// <summary>
		/// Inserts a child view at the specified index.
		/// </summary>
		/// <param name="index">The index at which to specify the child view.</param>
		/// <param name="child">The child view to insert.</param>
		public void Insert(int index, IView child)
		{
			if (child == null)
			{
				return;
			}
 
			_children.Insert(index, child);
 
			if (child is Element element)
			{
				InsertLogicalChild(index, element);
			}
 
			OnInsert(index, child);
		}
 
		/// <summary>
		/// Removes a child view.
		/// </summary>
		/// <param name="child">The child view to remove.</param>
		/// <returns><see langword="true"/> if the view was removed successfully, otherwise <see langword="false"/>.</returns>
		public bool Remove(IView child)
		{
			if (child == null)
			{
				return false;
			}
 
			var index = _children.IndexOf(child);
 
			if (index == -1)
			{
				return false;
			}
 
			RemoveAt(index);
 
			return true;
		}
 
		/// <summary>
		/// Removes a child view at the specified index.
		/// </summary>
		/// <param name="index">The index at which to remove the child view.</param>
		public void RemoveAt(int index)
		{
			if (index >= Count)
			{
				return;
			}
 
			var child = _children[index];
 
			_children.RemoveAt(index);
 
			if (child is Element element)
			{
				RemoveLogicalChild(element);
			}
 
			OnRemove(index, child);
		}
 
		/// <summary>
		/// Invoked when <see cref="Add(IView)"/> is called and notifies the handler associated to this layout.
		/// </summary>
		/// <param name="index">The index at which the child view was inserted.</param>
		/// <param name="view">The child view which was inserted.</param>
		protected virtual void OnAdd(int index, IView view)
		{
			NotifyHandler(nameof(ILayoutHandler.Add), index, view);
		}
 
		/// <summary>
		/// Invoked when <see cref="Clear"/> is called and notifies the handler associated to this layout.
		/// </summary>
		protected virtual void OnClear()
		{
			Handler?.Invoke(nameof(ILayoutHandler.Clear));
		}
 
		/// <summary>
		/// Invoked when <see cref="Insert(int, IView)"/> is called and notifies the handler associated to this layout.
		/// </summary>
		/// <param name="index">The index at which the child view was inserted.</param>
		/// <param name="view">The child view which was inserted.</param>
		protected virtual void OnRemove(int index, IView view)
		{
			NotifyHandler(nameof(ILayoutHandler.Remove), index, view);
		}
 
		/// <summary>
		/// Invoked when <see cref="RemoveAt(int)"/> is called and notifies the handler associated to this layout.
		/// </summary>
		/// <param name="index">The index at which the child view was removed.</param>
		/// <param name="view">The child view which was removed.</param>
		protected virtual void OnInsert(int index, IView view)
		{
			NotifyHandler(nameof(ILayoutHandler.Insert), index, view);
		}
 
		/// <summary>
		/// Invoked when <see cref="this[int]"/> is called and notifies the handler associated to this layout.
		/// </summary>
		/// <param name="index">The index at which the child view was updated.</param>
		/// <param name="view">The new child view which was added at <paramref name="index"/>.</param>
		/// <param name="oldView">The previous child view which was at <paramref name="index"/>.</param>
		protected virtual void OnUpdate(int index, IView view, IView oldView)
		{
			NotifyHandler(nameof(ILayoutHandler.Update), index, view);
		}
 
		void NotifyHandler(string action, int index, IView view)
		{
			Handler?.Invoke(action, new Maui.Handlers.LayoutHandlerUpdate(index, view));
		}
 
		void IPaddingElement.OnPaddingPropertyChanged(Thickness oldValue, Thickness newValue)
		{
			(this as IView).InvalidateMeasure();
		}
 
		Thickness IPaddingElement.PaddingDefaultValueCreator()
		{
			return new Thickness(0);
		}
 
		public Graphics.Size CrossPlatformMeasure(double widthConstraint, double heightConstraint)
		{
			return LayoutManager.Measure(widthConstraint, heightConstraint);
		}
 
		public Graphics.Size CrossPlatformArrange(Graphics.Rect bounds)
		{
			return LayoutManager.ArrangeChildren(bounds);
		}
 
		/// <summary>Bindable property for <see cref="CascadeInputTransparent"/>.</summary>
		public static readonly BindableProperty CascadeInputTransparentProperty =
			BindableProperty.Create(nameof(CascadeInputTransparent), typeof(bool), typeof(Layout), true,
				propertyChanged: OnCascadeInputTransparentPropertyChanged);
 
		/// <summary>
		/// Gets or sets a value that controls whether child elements
		/// inherit the input transparency of this layout when the tranparency is <see langword="true"/>.
		/// </summary>
		/// <value>
		/// <see langword="true" /> to cause child elements to inherit the input transparency of this layout,
		/// when this layout's <see cref="VisualElement.InputTransparent" /> property is <see langword="true" />.
		/// <see langword="false" /> to cause child elements to ignore the input tranparency of this layout.
		/// </value>
		public bool CascadeInputTransparent
		{
			get => (bool)GetValue(CascadeInputTransparentProperty);
			set => SetValue(CascadeInputTransparentProperty, value);
		}
 
		static void OnCascadeInputTransparentPropertyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			// We only need to update if the cascade changes anything, namely when InputTransparent=true.
			// When InputTransparent=false, then the cascade property has no effect.
			if (bindable is Layout layout && layout.InputTransparent)
			{
				layout.RefreshInputTransparentProperty();
			}
		}
	}
}