File: Handlers\Shell\ShellHandler.Windows.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using Microsoft.Maui.Controls.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using WBorder = Microsoft.UI.Xaml.Controls.Border;
using WScrollMode = Microsoft.UI.Xaml.Controls.ScrollMode;
 
namespace Microsoft.Maui.Controls.Handlers
{
	public partial class ShellHandler : ViewHandler<Shell, ShellView>
	{
		ScrollViewer _scrollViewer;
		double? _topAreaHeight = null;
		double? _headerHeight = null;
		double? _headerOffset = null;
 
		protected override ShellView CreatePlatformView()
		{
			var shellView = new ShellView();
			shellView.SetElement(VirtualView);
			return shellView;
		}
 
		protected override void ConnectHandler(ShellView platformView)
		{
			base.ConnectHandler(platformView);
 
			if (platformView is MauiNavigationView mauiNavigationView)
				mauiNavigationView.OnApplyTemplateFinished += OnApplyTemplateFinished;
 
			platformView.Loaded += OnLoaded;
			platformView.PaneOpened += OnPaneOpened;
			platformView.PaneOpening += OnPaneOpening;
			platformView.PaneClosing += OnPaneClosing;
			platformView.ItemInvoked += OnMenuItemInvoked;
		}
 
		private void OnLoaded(object sender, UI.Xaml.RoutedEventArgs e)
		{
			UpdateValue(nameof(Shell.FlyoutIcon));
			UpdateValue(nameof(Shell.FlyoutBackground));
		}
 
		protected override void DisconnectHandler(ShellView platformView)
		{
			base.DisconnectHandler(platformView);
 
			if (platformView is MauiNavigationView mauiNavigationView)
				mauiNavigationView.OnApplyTemplateFinished -= OnApplyTemplateFinished;
 
			platformView.Loaded -= OnLoaded;
			platformView.PaneOpened -= OnPaneOpened;
			platformView.PaneOpening -= OnPaneOpening;
			platformView.PaneClosing -= OnPaneClosing;
			platformView.ItemInvoked -= OnMenuItemInvoked;
		}
 
		void OnMenuItemInvoked(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewItemInvokedEventArgs args)
		{
			var item = args.InvokedItemContainer?.DataContext;
 
			if (item is NavigationViewItemViewModel nvm && nvm.Data is Element e)
				(VirtualView as IShellController)?.OnFlyoutItemSelected(e);
			else if (item is Element e2)
				(VirtualView as IShellController)?.OnFlyoutItemSelected(e2);
		}
 
		void OnApplyTemplateFinished(object sender, System.EventArgs e)
		{
			if (PlatformView == null)
				return;
 
			_scrollViewer = PlatformView.MenuItemsScrollViewer;
 
			UpdateValue(nameof(Shell.FlyoutHeaderBehavior));
		}
 
		void OnPaneOpened(UI.Xaml.Controls.NavigationView sender, object args)
		{
			PlatformView.UpdateFlyoutBackdrop();
		}
 
		void OnPaneClosing(UI.Xaml.Controls.NavigationView sender, UI.Xaml.Controls.NavigationViewPaneClosingEventArgs args)
		{
			args.Cancel = true;
			VirtualView.FlyoutIsPresented = false;
		}
 
		void OnPaneOpening(UI.Xaml.Controls.NavigationView sender, object args)
		{
			UpdateValue(nameof(Shell.FlyoutBackground));
			UpdateValue(nameof(Shell.FlyoutVerticalScrollMode));
			PlatformView.UpdateFlyoutBackdrop();
			PlatformView.UpdateFlyoutPosition();
			VirtualView.FlyoutIsPresented = true;
		}
 
		public override void SetVirtualView(IView view)
		{
			base.SetVirtualView(view);
 
			if (PlatformView.Element != view)
				PlatformView.SetElement((Shell)view);
		}
 
		public static void MapFlyoutBackdrop(ShellHandler handler, Shell view)
		{
			if (Brush.IsNullOrEmpty(view.FlyoutBackdrop))
				handler.PlatformView.FlyoutBackdrop = null;
			else
				handler.PlatformView.FlyoutBackdrop = view.FlyoutBackdrop;
		}
 
		public static void MapCurrentItem(ShellHandler handler, Shell view)
		{
			handler.PlatformView.SwitchShellItem(view.CurrentItem, true);
		}
 
		public static void MapFlyoutBackground(ShellHandler handler, Shell view)
		{
			handler.PlatformView.UpdatePaneBackground(
				!Brush.IsNullOrEmpty(view.FlyoutBackground) ?
					view.FlyoutBackground :
					view.FlyoutBackgroundColor?.AsPaint());
		}
 
		public static void MapFlyoutIcon(ShellHandler handler, Shell view)
		{
			var flyoutIcon = view.FlyoutIcon;
			var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
 
			handler.PlatformView.UpdateFlyoutIconAsync(flyoutIcon, provider).FireAndForget();
		}
 
		public static void MapFlyoutVerticalScrollMode(ShellHandler handler, Shell view)
		{
			handler.PlatformView.UpdateFlyoutVerticalScrollMode((WScrollMode)(int)view.FlyoutVerticalScrollMode);
		}
 
		public static void MapFlyout(ShellHandler handler, IFlyoutView flyoutView)
		{
			if (handler.PlatformView is RootNavigationView rnv)
				rnv.FlyoutView = flyoutView.Flyout;
 
			handler.PlatformView.FlyoutCustomContent = flyoutView.Flyout?.ToPlatform(handler.MauiContext);
 
		}
 
		public static void MapIsPresented(ShellHandler handler, IFlyoutView flyoutView)
		{
			// WinUI Will close the pane inside of the apply template code
			// so we wait until the control is loaded before applying IsPresented
			if (handler.PlatformView.IsLoaded)
				handler.PlatformView.IsPaneOpen = flyoutView.IsPresented;
		}
 
		public static void MapFlyoutWidth(ShellHandler handler, IFlyoutView flyoutView)
		{
			handler.PlatformView.UpdateFlyoutWidth(flyoutView);
		}
 
		public static void MapFlyoutBehavior(ShellHandler handler, IFlyoutView flyoutView)
		{
			handler.PlatformView.UpdateFlyoutBehavior(flyoutView);
		}
 
		public static void MapFlyoutFooter(ShellHandler handler, Shell view)
		{
			if (handler.PlatformView.PaneFooter == null)
				handler.PlatformView.PaneFooter = new ShellFooterView(view);
		}
 
		public static void MapFlyoutHeader(ShellHandler handler, Shell view)
		{
			if (handler.PlatformView.PaneHeader == null)
				handler.PlatformView.PaneHeader = new ShellHeaderView(view);
		}
 
		public static void MapFlyoutHeaderBehavior(ShellHandler handler, Shell view)
		{
			handler.UpdateFlyoutHeaderBehavior(view);
		}
 
		public static void MapItems(ShellHandler handler, Shell view)
		{
			handler.PlatformView.UpdateMenuItemSource();
			handler.UpdateValue(nameof(Shell.CurrentItem));
		}
 
		public static void MapFlyoutItems(ShellHandler handler, Shell view)
		{
			handler.PlatformView.UpdateMenuItemSource();
			handler.UpdateValue(nameof(Shell.CurrentItem));
		}
 
		void UpdateFlyoutHeaderBehavior(Shell view)
		{
			var flyoutHeader = (ShellHeaderView)PlatformView.PaneHeader;
 
			if (view.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Default ||
				view.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Fixed)
			{
				var defaultHeight = _headerHeight;
				var defaultTranslateY = _headerOffset;
 
				UpdateFlyoutHeaderTransformation(flyoutHeader, defaultHeight, defaultTranslateY);
				return;
			}
 
			if (_scrollViewer != null)
			{
				_scrollViewer.ViewChanged -= OnScrollViewerViewChanged;
				_scrollViewer.ViewChanged += OnScrollViewerViewChanged;
			}
		}
 
		void UpdateFlyoutHeaderTransformation(ShellHeaderView flyoutHeader, double? height, double? translationY)
		{
			if (translationY.HasValue)
			{
				flyoutHeader.RenderTransform = new CompositeTransform
				{
					TranslateY = translationY.Value
				};
			}
 
			if (height.HasValue)
			{
				flyoutHeader.Height = height.Value;
			}
		}
 
		void OnScrollViewerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
		{
			if (_scrollViewer == null)
				return;
 
			var flyoutHeader = PlatformView?.PaneHeader as ShellHeaderView;
 
			if (flyoutHeader == null)
				return;
 
			if (_headerHeight == null)
				_headerHeight = flyoutHeader.ActualHeight;
 
			if (_headerOffset == null)
			{
				if (flyoutHeader.RenderTransform is CompositeTransform compositeTransform)
					_headerOffset = compositeTransform.TranslateY;
				else
					_headerOffset = 0;
			}
 
			switch (VirtualView?.FlyoutHeaderBehavior)
			{
				case FlyoutHeaderBehavior.Scroll:
					var scrollHeight = Math.Max(_headerHeight.Value - _scrollViewer.VerticalOffset, 0);
					var scrollTranslateY = -_scrollViewer.VerticalOffset;
 
					UpdateFlyoutHeaderTransformation(flyoutHeader, scrollHeight, scrollTranslateY);
					break;
				case FlyoutHeaderBehavior.CollapseOnScroll:
 
					if (_topAreaHeight == null)
						_topAreaHeight = Math.Max(PlatformView.TopNavArea?.ActualHeight ?? 0, _headerHeight.Value);
 
					var calculatedHeight = _headerHeight.Value - _scrollViewer.VerticalOffset;
					var collapseOnScrollHeight = calculatedHeight < _topAreaHeight.Value ? _topAreaHeight.Value : calculatedHeight;
 
					var offsetY = -_scrollViewer.VerticalOffset;
					var maxOffsetY = -_topAreaHeight.Value;
					var collapseOnScrollTranslateY = offsetY < maxOffsetY ? maxOffsetY : offsetY;
 
					UpdateFlyoutHeaderTransformation(flyoutHeader, collapseOnScrollHeight, collapseOnScrollTranslateY);
					break;
			}
		}
	}
}