File: Handlers\Shell\Windows\ShellView.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Maui.Controls.Handlers;
using Microsoft.Maui.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
 
namespace Microsoft.Maui.Controls.Platform
{
	[Microsoft.UI.Xaml.Data.Bindable]
	public partial class ShellView : RootNavigationView, IAppearanceObserver, IFlyoutBehaviorObserver
	{
		internal static readonly global::Windows.UI.Color DefaultBackgroundColor = global::Windows.UI.Color.FromArgb(255, 3, 169, 244);
		internal static readonly global::Windows.UI.Color DefaultForegroundColor = Microsoft.UI.Colors.White;
		internal static readonly global::Windows.UI.Color DefaultTitleColor = Microsoft.UI.Colors.White;
		internal static readonly global::Windows.UI.Color DefaultUnselectedColor = global::Windows.UI.Color.FromArgb(180, 255, 255, 255);
		double _flyoutHeight = -1d;
		double _flyoutWidth = -1d;
 
		List<List<Element>> _flyoutGrouping;
		ShellItemHandler ItemRenderer { get; set; }
		IShellController ShellController => (IShellController)Element;
		ObservableCollection<object> FlyoutItems = new ObservableCollection<object>();
		IMauiContext MauiContext => Element.Handler.MauiContext;
		ShellSplitView _shellSplitView;
		Brush _flyoutBackdrop;
 
		public ShellView()
		{
			IsPaneOpen = false;
			MenuItemTemplateSelector = CreateShellFlyoutTemplateSelector();
			MenuItemsSource = FlyoutItems;
			this.Loaded += OnLoaded;
		}
 
		void OnLoaded(object sender, RoutedEventArgs e)
		{
			// We can't reliably set IsPaneOpen to true until the control has loaded
			// If we set it earlier than this then WinUI will transition it back to false
			if (IsPaneOpen != Element.FlyoutIsPresented)
				IsPaneOpen = Element.FlyoutIsPresented;
 
			UpdateFlyoutBackdrop();
		}
 
		private protected override void UpdateFlyoutCustomContent()
		{
			base.UpdateFlyoutCustomContent();
 
			if (FlyoutCustomContent == null)
				MenuItemsSource = FlyoutItems;
			else
				MenuItemsSource = null;
		}
 
		internal void SetElement(VisualElement element)
		{
			if (Element != null && element != null)
				throw new NotSupportedException("Reuse of the Shell Renderer is not supported");
 
			Element = (Shell)element;
			ShellController.AddAppearanceObserver(this, Element);
		}
 
		internal Shell Element { get; set; }
 
		private protected override void OnApplyTemplateCore()
		{
			_shellSplitView = new ShellSplitView(RootSplitView);
			_shellSplitView.FlyoutBackdrop = _flyoutBackdrop;
			TogglePaneButton?.SetAutomationPropertiesAutomationId("OK");
 
			base.OnApplyTemplateCore();
			UpdateFlyoutBackdrop();
		}
 
		internal void UpdateFlyoutPosition()
		{
			if (Element.FlyoutBehavior == FlyoutBehavior.Disabled)
				return;
 
			var splitView = _shellSplitView;
			if (splitView != null)
			{
				_shellSplitView.SetFlyoutSizes(_flyoutHeight, _flyoutWidth);
				if (IsPaneOpen)
					_shellSplitView.RefreshFlyoutPosition();
			}
		}
 
		internal void UpdateFlyoutBackdrop()
		{
			if (RootSplitView != null && Element.FlyoutBehavior != FlyoutBehavior.Flyout)
				return;
 
			var splitView = _shellSplitView;
			if (splitView != null)
			{
				if (IsPaneOpen)
					_shellSplitView.RefreshFlyoutBackdrop();
			}
		}
 
		internal Brush FlyoutBackdrop
		{
			set
			{
				_flyoutBackdrop = value;
 
				if (_shellSplitView != null)
					_shellSplitView.FlyoutBackdrop = value;
			}
		}
 
		internal void UpdateMenuItemSource()
		{
			var newGrouping = ((IShellController)Element).GenerateFlyoutGrouping();
			if (_flyoutGrouping != newGrouping)
			{
				_flyoutGrouping = newGrouping;
				var newItems = IterateItems(newGrouping).ToList();
 
				foreach (var item in newItems)
				{
					if (!FlyoutItems.Contains(item))
					{
						FlyoutItems.Add(item);
					}
				}
 
				for (var i = FlyoutItems.Count - 1; i >= 0; i--)
				{
					var item = FlyoutItems[i];
					if (!newItems.Contains(item))
						FlyoutItems.RemoveAt(i);
				}
			}
 
			if (!FlyoutItems.Contains(SelectedItem))
				SelectedItem = null;
		}
 
		IEnumerable<object> IterateItems(List<List<Element>> groups)
		{
			int separatorNumber = 0;
			foreach (var group in groups)
			{
				if (group.Count > 0 && group != groups[0])
				{
					yield return new FlyoutItemMenuSeparator(separatorNumber++); // Creates a separator
				}
 
				foreach (var item in group)
				{
					bool foundExistingVM = false;
 
					// Check to see if this element already has a VM counter part
					foreach (var navItem in FlyoutItems)
					{
						if (navItem is NavigationViewItemViewModel viewModel && viewModel.Data == item)
						{
							foundExistingVM = true;
							yield return viewModel;
						}
					}
 
					if (!foundExistingVM)
					{
						yield return new NavigationViewItemViewModel()
						{
							Data = item
						};
					}
				}
			}
		}
 
		partial class FlyoutItemMenuSeparator : Microsoft.UI.Xaml.Controls.MenuFlyoutSeparator
		{
			public FlyoutItemMenuSeparator(int separatorNumber)
			{
				Id = separatorNumber;
			}
 
			public int Id { get; set; }
			public override int GetHashCode() => Id.GetHashCode();
			public override bool Equals(object obj) =>
				obj is FlyoutItemMenuSeparator fim && fim.Id == Id;
		}
 
		internal void SwitchShellItem(ShellItem newItem, bool animate = true)
		{
			var navItems = FlyoutItems.OfType<NavigationViewItemViewModel>();
 
			// Implicit items aren't items that are surfaced to the user 
			// or data structures. So, we just want to find the element
			// the user defined on Shell
			if (Routing.IsImplicit(newItem))
			{
				if (Routing.IsImplicit(newItem.CurrentItem))
					SelectedItem = navItems.GetWithData(newItem.CurrentItem.CurrentItem);
				else
					SelectedItem = navItems.GetWithData(newItem.CurrentItem);
			}
			else
			{
				if (navItems.TryGetWithData(newItem, out NavigationViewItemViewModel vm1))
					SelectedItem = vm1;
				else if (newItem.CurrentItem is not null)
				{
					if (navItems.TryGetWithData(newItem.CurrentItem, out NavigationViewItemViewModel vm2))
						SelectedItem = vm2;
					else if (navItems.TryGetWithData(newItem.CurrentItem.CurrentItem, out NavigationViewItemViewModel vm3))
						SelectedItem = vm3;
				}
			}
 
			var handler = CreateShellItemView();
			if (handler.VirtualView != newItem)
				handler.SetVirtualView(newItem);
		}
 
		#region IAppearanceObserver
 
		void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
		{
			global::Windows.UI.Color backgroundColor = DefaultBackgroundColor;
			global::Windows.UI.Color titleColor = DefaultTitleColor;
 
			if (appearance != null)
			{
				if (!appearance.BackgroundColor.IsDefault())
					backgroundColor = appearance.BackgroundColor.ToWindowsColor();
				if (!appearance.TitleColor.IsDefault())
					titleColor = appearance.TitleColor.ToWindowsColor();
 
				_flyoutWidth = appearance.FlyoutWidth;
				_flyoutHeight = appearance.FlyoutHeight;
			}
 
			UpdateFlyoutBackdrop();
			UpdateFlyoutPosition();
		}
 
		#endregion IAppearanceObserver
 
		void IFlyoutBehaviorObserver.OnFlyoutBehaviorChanged(FlyoutBehavior behavior)
		{
		}
 
		public virtual ShellFlyoutTemplateSelector CreateShellFlyoutTemplateSelector() => new ShellFlyoutTemplateSelector();
 
		ShellItemHandler CreateShellItemView()
		{
			if (ItemRenderer == null)
			{
				ItemRenderer = (ShellItemHandler)Element.CurrentItem.ToHandler(MauiContext);
				if (ItemRenderer.PlatformView is NavigationView nv)
				{
					nv.SelectionChanged += TabSelectionChanged;
				}
			}
 
			if (ItemRenderer.PlatformView != (Content as FrameworkElement))
				Content = ItemRenderer.PlatformView;
 
			if (ItemRenderer.VirtualView != Element.CurrentItem)
				ItemRenderer.SetVirtualView(Element.CurrentItem);
 
			return ItemRenderer;
		}
 
		void TabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) =>
			Element?.Handler?.UpdateValue(nameof(Shell.CurrentItem));
	}
}