File: DualScreenInfo.cs
Web Access
Project: src\src\Controls\Foldable\src\Controls.Foldable.csproj (Microsoft.Maui.Controls.Foldable)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Foldable;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Foldable
{
	/// <summary>
	/// Event args for <see cref="Microsoft.Maui.Foldable.DualScreenInfo"/>'s 
	/// HingeAngleChanged event on foldable devices.
	/// </summary>
	public class HingeAngleChangedEventArgs : EventArgs
	{
		/// <summary>
		/// Creates a new HingeAngleChangedEventArgs specifying 
		/// the angle of the hinge between the screens.
		/// </summary>
		public HingeAngleChangedEventArgs(double hingeAngleInDegrees)
		{
			HingeAngleInDegrees = hingeAngleInDegrees;
		}
 
		/// <summary>
		/// Current angle of the hinge between the screens on a foldable device (0 - 360 degrees).
		/// </summary>
		public double HingeAngleInDegrees { get; }
	}
 
	/// <summary>
	/// Provides details about the visible areas a layout occupies so the content 
	/// can be positioned around where the screens split.
	/// </summary>
	public partial class DualScreenInfo : INotifyPropertyChanged
	{
		Rect[] _spanningBounds;
		Rect _hingeBounds;
		bool _isLandscape;
		TwoPaneViewMode _spanMode;
		TwoPaneViewLayoutGuide _twoPaneViewLayoutGuide;
		IFoldableService _dualScreenService;
		IFoldableService FoldableService =>
			_dualScreenService ?? Element?.Handler?.MauiContext?.Services?.GetService<IFoldableService>();
 
		internal VisualElement Element { get; }
 
		static Lazy<DualScreenInfo> _dualScreenInfo { get; } = new Lazy<DualScreenInfo>(OnCreate);
 
		/// <summary>
		/// Singleton that provides information about how the app window is 
		/// positioned across both screens.
		/// </summary>
		public static DualScreenInfo Current => _dualScreenInfo.Value;
		/// <summary>
		/// Used to watch for changes of any properties.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;
 
		public DualScreenInfo(VisualElement element) : this(element, null)
		{
		}
 
		internal DualScreenInfo(VisualElement element, IFoldableService dualScreenService)
		{
			_spanningBounds = Array.Empty<Rect>();
			Element = element;
			_dualScreenService = dualScreenService;
 
			if (element == null)
			{
				_twoPaneViewLayoutGuide = TwoPaneViewLayoutGuide.Instance;
			}
			else
			{
				_twoPaneViewLayoutGuide = new TwoPaneViewLayoutGuide(element, FoldableService); // get if null
				_twoPaneViewLayoutGuide.PropertyChanged += OnTwoPaneViewLayoutGuideChanged;
			}
		}
 
		internal void SetFoldableService(IFoldableService foldableService)
		{
			_dualScreenService = foldableService;
			_twoPaneViewLayoutGuide.SetFoldableService(foldableService);
		}
 
		EventHandler<HingeAngleChangedEventArgs> _hingeAngleChanged;
		int subscriberCount = 0;
		/// <summary>
		/// Triggered whenever the hinge angle is changed as a device is 
		/// folded or unfolded.
		/// </summary>
		public event EventHandler<HingeAngleChangedEventArgs> HingeAngleChanged
		{
			add
			{
				ProcessHingeAngleSubscriberCount(Interlocked.Increment(ref subscriberCount));
				_hingeAngleChanged += value;
			}
			remove
			{
				ProcessHingeAngleSubscriberCount(Interlocked.Decrement(ref subscriberCount));
				_hingeAngleChanged -= value;
			}
		}
 
		/// <summary>
		/// When spanned across two regions this will return a 
		/// <see cref="Microsoft.Maui.Graphics.Rect"/> 
		/// for each region. If not spanned this will return an empty array.
		/// </summary>
		public Rect[] SpanningBounds
		{
			get => GetSpanningBounds();
			set
			{
				if (_spanningBounds == null && value == null)
					return;
 
				if (_spanningBounds == null ||
					value == null ||
					!Enumerable.SequenceEqual(_spanningBounds, value))
				{
					SetProperty(ref _spanningBounds, value);
				}
			}
		}
 
		/// <summary>
		/// A <see cref="Microsoft.Maui.Graphics.Rect"/> 
		/// representing the absolute screen positioning of the device's hinge.
		/// </summary>
		public Rect HingeBounds
		{
			get => GetHingeBounds();
			set
			{
				SetProperty(ref _hingeBounds, value);
			}
		}
 
		/// <summary>Indicates if the device is Landscape.</summary>
		/// <remarks>
		/// Used when app is detected to be on a single screen - 
		/// mainly on Surface Duo (although possible also on other
		/// foldable with multi-window enabled)
		/// </remarks>
		public bool IsLandscape
		{
			get => GetIsLandscape();
			set
			{
				SetProperty(ref _isLandscape, value);
			}
		}
 
		/// <summary>
		/// Determines the layout direction of the panes:
		/// SinglePane, Wide, Tall
		/// </summary>
		public TwoPaneViewMode SpanMode
		{
			get => GetSpanMode();
			set
			{
				SetProperty(ref _spanMode, value);
			}
		}
 
		Rect[] GetSpanningBounds()
		{
			var guide = _twoPaneViewLayoutGuide;
			var hinge = guide.Hinge;
 
			if (hinge == Rect.Zero)
				return Array.Empty<Rect>();
 
			if (guide.Pane2 == Rect.Zero)
				return Array.Empty<Rect>();
 
			//TODO: I think this should this be checking SpanMode==Wide
			if (IsLandscape)
				return new[] { guide.Pane1, new Rect(0, hinge.Height + guide.Pane1.Height, guide.Pane2.Width, guide.Pane2.Height) };
			else
				return new[] { guide.Pane1, new Rect(hinge.Width + guide.Pane1.Width, 0, guide.Pane2.Width, guide.Pane2.Height) };
		}
 
		Rect GetHingeBounds()
		{
			var guide = _twoPaneViewLayoutGuide;
			return guide.Hinge;
		}
 
		bool GetIsLandscape() => _twoPaneViewLayoutGuide.IsLandscape;
 
		TwoPaneViewMode GetSpanMode() => _twoPaneViewLayoutGuide.Mode;
 
		static DualScreenInfo OnCreate()
		{
			DualScreenInfo dualScreenInfo = new DualScreenInfo(null);
			dualScreenInfo._twoPaneViewLayoutGuide.PropertyChanged += dualScreenInfo.OnTwoPaneViewLayoutGuideChanged;
			return dualScreenInfo;
		}
 
		void OnTwoPaneViewLayoutGuideChanged(object sender, PropertyChangedEventArgs e)
		{
			SpanningBounds = GetSpanningBounds();
			IsLandscape = GetIsLandscape();
			HingeBounds = GetHingeBounds();
			SpanMode = GetSpanMode();
		}
 
		bool SetProperty<T>(ref T backingStore, T value,
			[CallerMemberName] string propertyName = "",
			Action onChanged = null)
		{
			if (EqualityComparer<T>.Default.Equals(backingStore, value))
				return false;
 
			backingStore = value;
			onChanged?.Invoke();
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
			return true;
		}
 
#if !ANDROID
		public Task<int> GetHingeAngleAsync() => FoldableService?.GetHingeAngleAsync() ?? Task.FromResult(0);
		void ProcessHingeAngleSubscriberCount(int newCount) { }
#else

		static object hingeAngleLock = new object();
		/// <summary>
		/// Query the current hinge angle of the foldable device.
		/// </summary>
		/// <returns>Hinge angle between 0 and 360 degrees.</returns>
		public Task<int> GetHingeAngleAsync() => FoldableService?.GetHingeAngleAsync() ?? Task.FromResult(0);
 
		void ProcessHingeAngleSubscriberCount(int newCount)
		{
			lock (hingeAngleLock)
			{
				if (newCount == 1)
				{
					Foldable.FoldableService.HingeAngleChanged += OnHingeAngleChanged;
				}
				else if (newCount == 0)
				{
					Foldable.FoldableService.HingeAngleChanged -= OnHingeAngleChanged;
				}
			}
		}
 
		void OnHingeAngleChanged(object sender, HingeSensor.HingeSensorChangedEventArgs e)
		{
			_hingeAngleChanged?.Invoke(this, new HingeAngleChangedEventArgs(e.HingeAngle));
		}
#endif
	}
 
	/// <summary>
	/// Microsoft.Maui.Graphics.Rectangle to string (for debugging)
	/// </summary>
	static class BoundsExtensions
	{
		public static string ToRectStrings(this Rect[] bounds)
		{
			if (bounds.Length == 0)
				return "[]";
			string output = "";
			foreach (var rect in bounds)
			{
				output += rect.ToString() + " ";
			}
			return output;
		}
	}
}