// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; /// /// Renders MAUI views to Skia platform views. /// Handles the conversion of the view hierarchy. /// public class LinuxViewRenderer { private readonly IMauiContext _mauiContext; /// /// Static reference to the current MAUI Shell for navigation support. /// Used when Shell.Current is not available through normal lifecycle. /// public static Shell? CurrentMauiShell { get; private set; } /// /// Static reference to the current SkiaShell for navigation updates. /// public static SkiaShell? CurrentSkiaShell { get; private set; } /// /// Navigate to a route using the SkiaShell directly. /// Use this instead of Shell.Current.GoToAsync on Linux. /// /// The route to navigate to (e.g., "Buttons" or "//Buttons") /// True if navigation succeeded public static bool NavigateToRoute(string route) { if (CurrentSkiaShell == null) { Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null"); return false; } // Clean up the route - remove leading // or / var cleanRoute = route.TrimStart('/'); Console.WriteLine($"[NavigateToRoute] Navigating to: {cleanRoute}"); for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) { var section = CurrentSkiaShell.Sections[i]; if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) || section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}"); CurrentSkiaShell.NavigateToSection(i); return true; } } Console.WriteLine($"[NavigateToRoute] Route not found: {cleanRoute}"); return false; } /// /// Current renderer instance for page rendering. /// public static LinuxViewRenderer? CurrentRenderer { get; set; } /// /// Pushes a page onto the navigation stack. /// /// The page to push /// True if successful public static bool PushPage(Page page) { Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}"); if (CurrentSkiaShell == null) { Console.WriteLine($"[PushPage] CurrentSkiaShell is null"); return false; } if (CurrentRenderer == null) { Console.WriteLine($"[PushPage] CurrentRenderer is null"); return false; } try { // Render the page content SkiaView? pageContent = null; if (page is ContentPage contentPage && contentPage.Content != null) { pageContent = CurrentRenderer.RenderView(contentPage.Content); } if (pageContent == null) { Console.WriteLine($"[PushPage] Failed to render page content"); return false; } // Wrap in ScrollView if needed if (pageContent is not SkiaScrollView) { var scrollView = new SkiaScrollView { Content = pageContent }; pageContent = scrollView; } // Push onto SkiaShell's navigation stack CurrentSkiaShell.PushAsync(pageContent, page.Title ?? "Detail"); Console.WriteLine($"[PushPage] Successfully pushed page"); return true; } catch (Exception ex) { Console.WriteLine($"[PushPage] Error: {ex.Message}"); return false; } } /// /// Pops the current page from the navigation stack. /// /// True if successful public static bool PopPage() { Console.WriteLine($"[PopPage] Popping page"); if (CurrentSkiaShell == null) { Console.WriteLine($"[PopPage] CurrentSkiaShell is null"); return false; } return CurrentSkiaShell.PopAsync(); } public LinuxViewRenderer(IMauiContext mauiContext) { _mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext)); // Store reference for push/pop navigation CurrentRenderer = this; } /// /// Renders a MAUI page and returns the corresponding SkiaView. /// public SkiaView? RenderPage(Page page) { if (page == null) return null; // Special handling for Shell - Shell is our navigation container if (page is Shell shell) { return RenderShell(shell); } // Set handler context page.Handler?.DisconnectHandler(); var handler = page.ToHandler(_mauiContext); if (handler.PlatformView is SkiaView skiaPage) { // For ContentPage, render the content if (page is ContentPage contentPage && contentPage.Content != null) { var contentView = RenderView(contentPage.Content); if (skiaPage is SkiaPage sp && contentView != null) { sp.Content = contentView; } } return skiaPage; } return null; } /// /// Renders a MAUI Shell with all its navigation structure. /// private SkiaShell RenderShell(Shell shell) { // Store reference for navigation - Shell.Current is computed from Application.Current.Windows // Our platform handles navigation through SkiaShell directly CurrentMauiShell = shell; var skiaShell = new SkiaShell { Title = shell.Title ?? "App", FlyoutBehavior = shell.FlyoutBehavior switch { FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout, FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked, FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled, _ => ShellFlyoutBehavior.Flyout } }; // Process shell items into sections foreach (var item in shell.Items) { ProcessShellItem(skiaShell, item); } // Store reference to SkiaShell for navigation CurrentSkiaShell = skiaShell; // Subscribe to MAUI Shell navigation events to update SkiaShell shell.Navigated += OnShellNavigated; shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}"); Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}"); for (int i = 0; i < skiaShell.Sections.Count; i++) { Console.WriteLine($"[Navigation] Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'"); } return skiaShell; } /// /// Handles MAUI Shell navigation events and updates SkiaShell accordingly. /// private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) { Console.WriteLine($"[Navigation] OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}"); if (CurrentSkiaShell == null || CurrentMauiShell == null) { Console.WriteLine($"[Navigation] CurrentSkiaShell or CurrentMauiShell is null"); return; } // Get the current route from the Shell var currentState = CurrentMauiShell.CurrentState; var location = currentState?.Location?.OriginalString ?? ""; Console.WriteLine($"[Navigation] Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}"); // Find the matching section in SkiaShell by route for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) { var section = CurrentSkiaShell.Sections[i]; Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'"); if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"[Navigation] Match found by route! Navigating to section {i}"); if (i != CurrentSkiaShell.CurrentSectionIndex) { CurrentSkiaShell.NavigateToSection(i); } return; } if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}"); if (i != CurrentSkiaShell.CurrentSectionIndex) { CurrentSkiaShell.NavigateToSection(i); } return; } } Console.WriteLine($"[Navigation] No matching section found for location: {location}"); } /// /// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections. /// private void ProcessShellItem(SkiaShell skiaShell, ShellItem item) { if (item is FlyoutItem flyoutItem) { // Each FlyoutItem becomes a section var section = new ShellSection { Title = flyoutItem.Title ?? "", Route = flyoutItem.Route ?? flyoutItem.Title ?? "" }; // Process the items within the FlyoutItem foreach (var shellSection in flyoutItem.Items) { foreach (var content in shellSection.Items) { var shellContent = new ShellContent { Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "", Route = content.Route ?? "" }; // Create the page content var pageContent = CreateShellContentPage(content); if (pageContent != null) { shellContent.Content = pageContent; } section.Items.Add(shellContent); } } // If there's only one item, use it as the main section content if (section.Items.Count == 1) { section.Title = section.Items[0].Title; } skiaShell.AddSection(section); } else if (item is TabBar tabBar) { // TabBar items get their own sections foreach (var tab in tabBar.Items) { var section = new ShellSection { Title = tab.Title ?? "", Route = tab.Route ?? "" }; foreach (var content in tab.Items) { var shellContent = new ShellContent { Title = content.Title ?? tab.Title ?? "", Route = content.Route ?? "" }; var pageContent = CreateShellContentPage(content); if (pageContent != null) { shellContent.Content = pageContent; } section.Items.Add(shellContent); } skiaShell.AddSection(section); } } else { // Generic ShellItem var section = new ShellSection { Title = item.Title ?? "", Route = item.Route ?? "" }; foreach (var shellSection in item.Items) { foreach (var content in shellSection.Items) { var shellContent = new ShellContent { Title = content.Title ?? "", Route = content.Route ?? "" }; var pageContent = CreateShellContentPage(content); if (pageContent != null) { shellContent.Content = pageContent; } section.Items.Add(shellContent); } } skiaShell.AddSection(section); } } /// /// Creates the page content for a ShellContent. /// private SkiaView? CreateShellContentPage(Controls.ShellContent content) { try { // Try to create the page from the content template Page? page = null; if (content.ContentTemplate != null) { page = content.ContentTemplate.CreateContent() as Page; } if (page == null && content.Content is Page contentPage) { page = contentPage; } if (page is ContentPage cp && cp.Content != null) { // Wrap in a scroll view if not already scrollable var contentView = RenderView(cp.Content); if (contentView != null) { if (contentView is SkiaScrollView) { return contentView; } else { var scrollView = new SkiaScrollView { Content = contentView }; return scrollView; } } } } catch (Exception) { // Silently handle template creation errors } return null; } /// /// Renders a MAUI view and returns the corresponding SkiaView. /// public SkiaView? RenderView(IView view) { if (view == null) return null; try { // Disconnect any existing handler if (view is Element element && element.Handler != null) { element.Handler.DisconnectHandler(); } // Create handler for the view // The handler's ConnectHandler and property mappers handle child views automatically var handler = view.ToHandler(_mauiContext); if (handler?.PlatformView is not SkiaView skiaView) { // If no Skia handler, create a fallback return CreateFallbackView(view); } // Handlers manage their own children via ConnectHandler and property mappers // No manual child rendering needed here - that caused "View already has a parent" errors return skiaView; } catch (Exception) { return CreateFallbackView(view); } } /// /// Creates a fallback view for unsupported view types. /// private SkiaView CreateFallbackView(IView view) { // For views without handlers, create a placeholder return new SkiaLabel { Text = $"[{view.GetType().Name}]", TextColor = SKColors.Gray, FontSize = 12 }; } } /// /// Extension methods for MAUI handler creation. /// public static class MauiHandlerExtensions { /// /// Creates a handler for the view and returns it. /// public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext) { var handler = mauiContext.Handlers.GetHandler(element.GetType()); if (handler != null) { handler.SetMauiContext(mauiContext); handler.SetVirtualView(element); } return handler!; } }