File: TwoPaneView.cs
Web Access
Project: src\src\Controls\Foldable\src\Controls.Foldable.csproj (Microsoft.Maui.Controls.Foldable)
using System;
using System.Threading;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Foldable;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls.Foldable
{
	/// <summary>
	/// Layout container with two panes that will position the child content
	/// side-by-side or vertically. The relative size of the two panes can be 
	/// configured, but on a foldable Android device the split will be aligned 
	/// with the hinge or screen fold.
	/// </summary>
	/// <remarks>
	/// Requires <see cref="Microsoft.Maui.Foldable.HostBuilderExtensions.UseFoldable(Maui.Hosting.MauiAppBuilder)"/>
	/// be configured in the .NET MAUI app to configure the Android lifecycle
	/// to detect and adapt to foldable device hinges and screen folds.
	/// </remarks>
	[ContentProperty("")]
	public partial class TwoPaneView : Grid
	{
		enum ViewMode
		{
			Pane1Only,
			Pane2Only,
			LeftRight,
			RightLeft,
			TopBottom,
			BottomTop,
			None
		};
 
		TwoPaneViewLayoutGuide _twoPaneViewLayoutGuide;
		ContentView _content1;
		ContentView _content2;
		ViewMode _currentMode;
		bool _updatingMode = false;
		bool _processPendingChange = false;
		Rect _layoutGuidePane1;
		Rect _layoutGuidePane2;
		TwoPaneViewMode _layoutGuideMode;
		Rect _layoutGuideHinge;
		bool _layoutGuideIsLandscape;
		double _previousWidth = -1;
		double _previousHeight = -1;
 
		/// <summary>Bindable property for <see cref="TallModeConfiguration"/>.</summary>
		public static readonly BindableProperty TallModeConfigurationProperty
			= BindableProperty.Create("TallModeConfiguration", typeof(TwoPaneViewTallModeConfiguration), typeof(TwoPaneView), defaultValue: TwoPaneViewTallModeConfiguration.TopBottom, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="WideModeConfiguration"/>.</summary>
		public static readonly BindableProperty WideModeConfigurationProperty
			= BindableProperty.Create("WideModeConfiguration", typeof(TwoPaneViewWideModeConfiguration), typeof(TwoPaneView), defaultValue: TwoPaneViewWideModeConfiguration.LeftRight, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="Pane1"/>.</summary>
		public static readonly BindableProperty Pane1Property
			= BindableProperty.Create("Pane1", typeof(View), typeof(TwoPaneView), propertyChanged: (b, o, n) => OnPanePropertyChanged(b, o, n, 0));
 
		/// <summary>Bindable property for <see cref="Pane2"/>.</summary>
		public static readonly BindableProperty Pane2Property
			= BindableProperty.Create("Pane2", typeof(View), typeof(TwoPaneView), propertyChanged: (b, o, n) => OnPanePropertyChanged(b, o, n, 1));
 
		static readonly BindablePropertyKey ModePropertyKey
			= BindableProperty.CreateReadOnly("Mode", typeof(TwoPaneViewMode), typeof(TwoPaneView), defaultValue: TwoPaneViewMode.SinglePane, propertyChanged: OnModePropertyChanged);
 
		/// <summary>Bindable property for <see cref="Mode"/>.</summary>
		public static readonly BindableProperty ModeProperty = ModePropertyKey.BindableProperty;
 
		/// <summary>Bindable property for <see cref="PanePriority"/>.</summary>
		public static readonly BindableProperty PanePriorityProperty
			= BindableProperty.Create("PanePriority", typeof(TwoPaneViewPriority), typeof(TwoPaneView), defaultValue: TwoPaneViewPriority.Pane1, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="MinTallModeHeight"/>.</summary>
		public static readonly BindableProperty MinTallModeHeightProperty
			= BindableProperty.Create("MinTallModeHeight", typeof(double), typeof(TwoPaneView), defaultValueCreator: OnMinModePropertyCreate, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="MinWideModeWidth"/>.</summary>
		public static readonly BindableProperty MinWideModeWidthProperty
			= BindableProperty.Create("MinWideModeWidth", typeof(double), typeof(TwoPaneView), defaultValueCreator: OnMinModePropertyCreate, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="Pane1Length"/>.</summary>
		public static readonly BindableProperty Pane1LengthProperty
			= BindableProperty.Create("Pane1Length", typeof(GridLength), typeof(TwoPaneView), defaultValue: GridLength.Star, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>Bindable property for <see cref="Pane2Length"/>.</summary>
		public static readonly BindableProperty Pane2LengthProperty
			= BindableProperty.Create("Pane2Length", typeof(GridLength), typeof(TwoPaneView), defaultValue: GridLength.Star, propertyChanged: TwoPaneViewLayoutPropertyChanged);
 
		/// <summary>
		/// Event when the <see cref="Microsoft.Maui.Controls.Foldable.TwoPaneViewMode"/>
		/// changes on a foldable device.
		/// </summary>
		public event EventHandler ModeChanged;
 
		static object OnMinModePropertyCreate(BindableObject bindable)
		{
			double returnValue = 641d;
			if (DeviceDisplay.MainDisplayInfo.Density <= 0)
				return returnValue;
 
			returnValue = 641d / DeviceDisplay.MainDisplayInfo.Density;
			return returnValue;
		}
 
 
		static void OnModePropertyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			((TwoPaneView)bindable).ModeChanged?.Invoke(bindable, EventArgs.Empty);
		}
 
 
		static void TwoPaneViewLayoutPropertyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			var b = (TwoPaneView)bindable;
			b.UpdateMode();
		}
 
		static void OnPanePropertyChanged(BindableObject bindable, object oldValue, object newValue, int paneIndex)
		{
			TwoPaneView twoPaneView = (TwoPaneView)bindable;
			var newView = (View)newValue;
 
			if (paneIndex == 0)
				twoPaneView._content1.Content = newView;
			else
				twoPaneView._content2.Content = newView;
 
			twoPaneView.UpdateMode();
		}
 
		/// <summary>
		/// Gets or sets the minimum height at which panes are shown in tall mode.
		/// </summary>
		public double MinTallModeHeight
		{
			get { return (double)GetValue(MinTallModeHeightProperty); }
			set { SetValue(MinTallModeHeightProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets the minimum width at which panes are shown in wide mode.
		/// </summary>
		public double MinWideModeWidth
		{
			get { return (double)GetValue(MinWideModeWidthProperty); }
			set { SetValue(MinWideModeWidthProperty, value); }
		}
 
		/// <summary>
		/// Gets the calculated width (in wide mode) or height (in tall mode) of pane 1, or sets the GridLength value of pane 1.
		/// </summary>
		[System.ComponentModel.TypeConverter(typeof(GridLengthTypeConverter))]
		public GridLength Pane1Length
		{
			get { return (GridLength)GetValue(Pane1LengthProperty); }
			set { SetValue(Pane1LengthProperty, value); }
		}
 
		/// <summary>
		/// Gets the calculated width (in wide mode) or height (in tall mode) of pane 2, or sets the GridLength value of pane 2.
		/// </summary>
		[System.ComponentModel.TypeConverter(typeof(GridLengthTypeConverter))]
		public GridLength Pane2Length
		{
			get { return (GridLength)GetValue(Pane2LengthProperty); }
			set { SetValue(Pane2LengthProperty, value); }
		}
 
		/// <summary>
		/// Gets a <see cref="Microsoft.Maui.Controls.Foldable.TwoPaneViewMode"/> value
		/// that indicates how panes are shown.
		/// </summary>
		public TwoPaneViewMode Mode { get => (TwoPaneViewMode)GetValue(ModeProperty); }
 
		/// <summary>
		/// Gets or sets a value that indicates how panes are shown in tall mode.
		/// </summary>
		public TwoPaneViewTallModeConfiguration TallModeConfiguration
		{
			get { return (TwoPaneViewTallModeConfiguration)GetValue(TallModeConfigurationProperty); }
			set { SetValue(TallModeConfigurationProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets a value that indicates how panes are shown in wide mode.
		/// </summary>
		public TwoPaneViewWideModeConfiguration WideModeConfiguration
		{
			get { return (TwoPaneViewWideModeConfiguration)GetValue(WideModeConfigurationProperty); }
			set { SetValue(WideModeConfigurationProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets the content of pane 1.
		/// </summary>
		public View Pane1
		{
			get { return (View)GetValue(Pane1Property); }
			set { SetValue(Pane1Property, value); }
		}
 
		/// <summary>
		/// Gets or sets the content of pane 2.
		/// </summary>
		public View Pane2
		{
			get { return (View)GetValue(Pane2Property); }
			set { SetValue(Pane2Property, value); }
		}
 
		/// <summary>
		/// Gets or sets a value that indicates which pane has priority.
		/// </summary>
		public TwoPaneViewPriority PanePriority
		{
			get { return (TwoPaneViewPriority)GetValue(PanePriorityProperty); }
			set { SetValue(PanePriorityProperty, value); }
		}
 
		public TwoPaneView() : this(null)
		{
		}
 
		internal TwoPaneView(IFoldableService dualScreenService)
		{
			_twoPaneViewLayoutGuide = new TwoPaneViewLayoutGuide(this, dualScreenService);
			_content1 = new ContentView();
			_content2 = new ContentView();
 
			Children.Add(_content1);
			Children.Add(_content2);
 
			this.VerticalOptions = LayoutOptions.Fill;
			this.HorizontalOptions = LayoutOptions.Fill;
			ColumnSpacing = 0;
			RowSpacing = 0;
 
			RowDefinitions = new RowDefinitionCollection() { new RowDefinition(), new RowDefinition(), new RowDefinition() };
			ColumnDefinitions = new ColumnDefinitionCollection() { new ColumnDefinition(), new ColumnDefinition(), new ColumnDefinition() };
 
		}
 
		private protected override void OnHandlerChangingCore(HandlerChangingEventArgs args)
		{
			base.OnHandlerChangingCore(args);
 
			if (_twoPaneViewLayoutGuide.DualScreenService != null)
			{
				_twoPaneViewLayoutGuide.DualScreenService.OnLayoutChanged += DualScreenService_OnFeatureChanged;
			}
		}
 
		private void DualScreenService_OnFeatureChanged(object sender, FoldEventArgs e)
		{
			System.Diagnostics.Debug.Write("TwoPaneView.DualScreenService_OnFeatureChanged - " + e, "JWM");
			try
			{
				InvalidateMeasure();
			}
			catch (Exception)
			{
 
			}
		}
 
		internal override void OnIsPlatformEnabledChanged()
		{
			base.OnIsPlatformEnabledChanged();
			if (IsPlatformEnabled)
			{
				_twoPaneViewLayoutGuide.PropertyChanged += OnTwoPaneViewLayoutGuide;
			}
			else
			{
				_twoPaneViewLayoutGuide.PropertyChanged -= OnTwoPaneViewLayoutGuide;
			}
		}
 
		void OnTwoPaneViewLayoutGuide(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			if (_twoPaneViewLayoutGuide.Pane1 == _layoutGuidePane1 &&
				_twoPaneViewLayoutGuide.Pane2 == _layoutGuidePane2 &&
				_twoPaneViewLayoutGuide.Mode == _layoutGuideMode &&
				_twoPaneViewLayoutGuide.Hinge == _layoutGuideHinge &&
				_twoPaneViewLayoutGuide.IsLandscape == _layoutGuideIsLandscape)
			{
				return;
			}
 
			_layoutGuidePane1 = _twoPaneViewLayoutGuide.Pane1;
			_layoutGuidePane2 = _twoPaneViewLayoutGuide.Pane2;
			_layoutGuideMode = _twoPaneViewLayoutGuide.Mode;
			_layoutGuideHinge = _twoPaneViewLayoutGuide.Hinge;
			_layoutGuideIsLandscape = _twoPaneViewLayoutGuide.IsLandscape;
 
			InvalidateMeasure();
		}
 
		protected override Size MeasureOverride(double widthConstraint, double heightConstraint)
		{
			UpdateMode(widthConstraint, heightConstraint, false);
			var sizeRequest = base.MeasureOverride(widthConstraint, heightConstraint);
			return sizeRequest;
		}
 
		protected override Size ArrangeOverride(Rect bounds)
		{
			return base.ArrangeOverride(bounds);
		}
 
		void UpdateMode(bool invalidateLayout = true)
		{
			UpdateMode(Width, Height, invalidateLayout);
		}
 
		bool UpdateMode(double width, double height, bool invalidateLayout = true)
		{
			invalidateLayout = false;
 
			// controls hasn't fully been created yet
			if (RowDefinitions.Count != 3
				|| ColumnDefinitions.Count != 3
				|| width == -1
				|| height == -1
				|| width == double.PositiveInfinity
				|| height == double.PositiveInfinity)
			{
				return false;
			}
 
 
			if (_updatingMode)
			{
				_processPendingChange = true;
				return false;
			}
 
			_updatingMode = true;
			try
			{
				double controlWidth = width;
				double controlHeight = height;
				_previousWidth = width;
				_previousHeight = height;
 
				ViewMode newMode = (PanePriority == TwoPaneViewPriority.Pane1) ? ViewMode.Pane1Only : ViewMode.Pane2Only;
 
				_twoPaneViewLayoutGuide.UpdateLayouts(width, height);
 
				if (_twoPaneViewLayoutGuide.Mode != TwoPaneViewMode.SinglePane)
				{
					if (_twoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Wide)
					{
						// Regions are arranged horizontally
						if (WideModeConfiguration != TwoPaneViewWideModeConfiguration.SinglePane)
						{
							newMode = (WideModeConfiguration == TwoPaneViewWideModeConfiguration.LeftRight) ? ViewMode.LeftRight : ViewMode.RightLeft;
						}
					}
					else if (_twoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Tall)
					{
						// Regions are arranged vertically
						if (TallModeConfiguration != TwoPaneViewTallModeConfiguration.SinglePane)
						{
							newMode = (TallModeConfiguration == TwoPaneViewTallModeConfiguration.TopBottom) ? ViewMode.TopBottom : ViewMode.BottomTop;
						}
					}
				}
				else
				{
					// One region
					if (controlWidth > MinWideModeWidth && WideModeConfiguration != TwoPaneViewWideModeConfiguration.SinglePane)
					{
						// Split horizontally
						newMode = (WideModeConfiguration == TwoPaneViewWideModeConfiguration.LeftRight) ? ViewMode.LeftRight : ViewMode.RightLeft;
					}
					else if (controlHeight > MinTallModeHeight && TallModeConfiguration != TwoPaneViewTallModeConfiguration.SinglePane)
					{
						// Split vertically
						newMode = (TallModeConfiguration == TwoPaneViewTallModeConfiguration.TopBottom) ? ViewMode.TopBottom : ViewMode.BottomTop;
					}
				}
 
				// Update row/column sizes (this may need to happen even if the mode doesn't change)
				UpdateRowsColumns(newMode, width, height);
 
				// Update mode if necessary
				if (newMode != _currentMode)
				{
					_currentMode = newMode;
 
					TwoPaneViewMode newViewMode = TwoPaneViewMode.SinglePane;
 
					switch (_currentMode)
					{
						case ViewMode.Pane1Only:
							break;
						case ViewMode.Pane2Only:
							break;
						case ViewMode.LeftRight:
							newViewMode = TwoPaneViewMode.Wide;
							break;
						case ViewMode.RightLeft:
							newViewMode = TwoPaneViewMode.Wide;
							break;
						case ViewMode.TopBottom:
							newViewMode = TwoPaneViewMode.Tall;
							break;
						case ViewMode.BottomTop:
							newViewMode = TwoPaneViewMode.Tall;
							break;
					}
 
					if (newViewMode != Mode)
					{
						_updatingMode = false;
						SetValue(ModePropertyKey, newViewMode);
					}
				}
 
				_updatingMode = false;
 
				if (_processPendingChange)
				{
					_processPendingChange = false;
					UpdateMode();
				}
				else
				{
					if (invalidateLayout)
						InvalidateMeasure(); //HACK:FOLDABLE was InvalidateLayout();
				}
			}
			finally
			{
				_updatingMode = false;
			}
 
			return true;
		}
 
		Rect _previousHinge = Rect.Zero;
 
		void UpdateRowsColumns(ViewMode newMode, double newWidth, double newHeight)
		{
			var columnLeft = ColumnDefinitions[0];
			var columnMiddle = ColumnDefinitions[1];
			var columnRight = ColumnDefinitions[2];
 
			var rowTop = RowDefinitions[0];
			var rowMiddle = RowDefinitions[1];
			var rowBottom = RowDefinitions[2];
 
			Rect pane1 = _twoPaneViewLayoutGuide.Pane1;
			Rect pane2 = _twoPaneViewLayoutGuide.Pane2;
			bool isLayoutSpanned = _twoPaneViewLayoutGuide.Mode != TwoPaneViewMode.SinglePane;
 
			_previousHinge = _twoPaneViewLayoutGuide.Hinge;
 
			if (_twoPaneViewLayoutGuide.Mode != TwoPaneViewMode.SinglePane && newMode != ViewMode.Pane1Only && newMode != ViewMode.Pane2Only)
			{
				_previousHinge = _twoPaneViewLayoutGuide.Hinge;
				Rect hinge = _twoPaneViewLayoutGuide.Hinge;
 
				if (_twoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Wide)
				{
					columnMiddle.Width = new GridLength(hinge.Width, GridUnitType.Absolute);
					columnLeft.Width = new GridLength(pane1.Width, GridUnitType.Absolute);
					columnRight.Width = new GridLength(pane2.Width, GridUnitType.Absolute);
 
					rowMiddle.Height = new GridLength(0, GridUnitType.Absolute);
					rowTop.Height = GridLength.Star;
					rowBottom.Height = new GridLength(0, GridUnitType.Absolute);
				}
				else
				{
					rowMiddle.Height = new GridLength(hinge.Height, GridUnitType.Absolute);
					rowTop.Height = new GridLength(pane1.Height, GridUnitType.Absolute);
					rowBottom.Height = new GridLength(pane2.Height, GridUnitType.Absolute);
 
					columnMiddle.Width = new GridLength(0, GridUnitType.Absolute);
					columnLeft.Width = GridLength.Star;
					columnRight.Width = new GridLength(0, GridUnitType.Absolute);
				}
			}
			else
			{
				columnMiddle.Width = new GridLength(0, GridUnitType.Absolute);
				rowMiddle.Height = new GridLength(0, GridUnitType.Absolute);
 
				if (newMode == ViewMode.LeftRight || newMode == ViewMode.RightLeft)
				{
					columnLeft.Width = ((newMode == ViewMode.LeftRight) ? Pane1Length : Pane2Length);
					columnRight.Width = ((newMode == ViewMode.LeftRight) ? Pane2Length : Pane1Length);
				}
				else
				{
					columnLeft.Width = new GridLength(1, GridUnitType.Star);
					columnRight.Width = new GridLength(0, GridUnitType.Absolute);
				}
 
				if (newMode == ViewMode.TopBottom || newMode == ViewMode.BottomTop)
				{
					rowTop.Height = ((newMode == ViewMode.TopBottom) ? Pane1Length : Pane2Length);
					rowBottom.Height = ((newMode == ViewMode.TopBottom) ? Pane2Length : Pane1Length);
				}
				else
				{
					rowTop.Height = new GridLength(1, GridUnitType.Star);
					rowBottom.Height = new GridLength(0, GridUnitType.Absolute);
				}
			}
 
			switch (newMode)
			{
				case ViewMode.LeftRight:
					SetRowColumn(_content1, 0, 0);
					SetRowColumn(_content2, 0, 2);
					_content1.IsVisible = true;
					_content2.IsVisible = true;
 
					if (!isLayoutSpanned)
					{
						// add padding to content where the content is under the hinge
						_content1.Padding = new Thickness(pane1.X, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, newWidth - pane1.Width, 0);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					break;
				case ViewMode.RightLeft:
					SetRowColumn(_content1, 0, 2);
					SetRowColumn(_content2, 0, 0);
					_content1.IsVisible = true;
					_content2.IsVisible = true;
 
					if (!isLayoutSpanned)
					{
						// add padding to content where the content is under the hinge
						_content2.Padding = new Thickness(pane1.X, 0, 0, 0);
						_content1.Padding = new Thickness(0, 0, newWidth - pane1.Width, 0);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					break;
				case ViewMode.TopBottom:
					SetRowColumn(_content1, 0, 0);
					SetRowColumn(_content2, 2, 0);
					_content1.IsVisible = true;
					_content2.IsVisible = true;
 
					if (!isLayoutSpanned)
					{
						// add padding to content where the content is under the hinge
						_content1.Padding = new Thickness(0, pane1.Y, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, newHeight - pane1.Height);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
 
					break;
				case ViewMode.BottomTop:
					SetRowColumn(_content1, 2, 0);
					SetRowColumn(_content2, 0, 0);
					_content1.IsVisible = true;
					_content2.IsVisible = true;
 
					if (!isLayoutSpanned)
					{
						// add padding to content where the content is under the hinge
						_content2.Padding = new Thickness(0, pane1.Y, 0, 0);
						_content1.Padding = new Thickness(0, 0, 0, newHeight - pane1.Height);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					break;
				case ViewMode.Pane1Only:
					SetRowColumn(_content1, 0, 0);
					SetRowColumn(_content2, 0, 2);
					_content1.IsVisible = true;
					_content2.IsVisible = false;
					if (!isLayoutSpanned)
					{
						// add padding to content where the content is under the hinge
						_content1.Padding = new Thickness(pane1.X, pane1.Y, newWidth - pane1.Width - pane1.X, newHeight - pane1.Height - pane1.Y);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					break;
				case ViewMode.Pane2Only:
					SetRowColumn(_content1, 0, 2);
					SetRowColumn(_content2, 0, 0);
					_content1.IsVisible = false;
					_content2.IsVisible = true;
					if (!isLayoutSpanned)
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						// add padding to content where the content is under the hinge
						_content2.Padding = new Thickness(pane1.X, pane1.Y, newWidth - pane1.Width - pane1.X, newHeight - pane1.Height - pane1.Y);
					}
					else
					{
						_content1.Padding = new Thickness(0, 0, 0, 0);
						_content2.Padding = new Thickness(0, 0, 0, 0);
					}
					break;
			}
 
			void SetRowColumn(BindableObject bo, int row, int column)
			{
				if (bo == null)
					return;
 
				Grid.SetColumn(bo, column);
				Grid.SetRow(bo, row);
			}
		}
	}
}