diff --git a/Handlers/PageHandler.cs b/Handlers/PageHandler.cs index 4b33d64..54f65db 100644 --- a/Handlers/PageHandler.cs +++ b/Handlers/PageHandler.cs @@ -20,7 +20,9 @@ public partial class PageHandler : ViewHandler { [nameof(Page.Title)] = MapTitle, [nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource, + [nameof(Page.IconImageSource)] = MapIconImageSource, [nameof(Page.Padding)] = MapPadding, + [nameof(Page.IsBusy)] = MapIsBusy, [nameof(IView.Background)] = MapBackground, [nameof(VisualElement.BackgroundColor)] = MapBackgroundColor, }; @@ -114,6 +116,19 @@ public partial class PageHandler : ViewHandler Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}"); } } + + public static void MapIconImageSource(PageHandler handler, Page page) + { + // Icon is typically used by navigation containers (Shell, TabbedPage) + // Store for later use but don't render directly on the page + handler.PlatformView?.Invalidate(); + } + + public static void MapIsBusy(PageHandler handler, Page page) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsBusy = page.IsBusy; + } } /// @@ -125,6 +140,7 @@ public partial class ContentPageHandler : PageHandler new PropertyMapper(PageHandler.Mapper) { [nameof(ContentPage.Content)] = MapContent, + [nameof(ContentPage.ToolbarItems)] = MapToolbarItems, }; public static new CommandMapper CommandMapper = @@ -146,6 +162,17 @@ public partial class ContentPageHandler : PageHandler return new SkiaContentPage(); } + protected override void ConnectHandler(SkiaPage platformView) + { + base.ConnectHandler(platformView); + + // Sync toolbar items initially + if (VirtualView is ContentPage contentPage && platformView is SkiaContentPage skiaContentPage) + { + SyncToolbarItems(skiaContentPage, contentPage); + } + } + public static void MapContent(ContentPageHandler handler, ContentPage page) { if (handler.PlatformView is null || handler.MauiContext is null) return; @@ -177,4 +204,38 @@ public partial class ContentPageHandler : PageHandler handler.PlatformView.Content = null; } } + + public static void MapToolbarItems(ContentPageHandler handler, ContentPage page) + { + if (handler.PlatformView is not SkiaContentPage skiaContentPage) return; + SyncToolbarItems(skiaContentPage, page); + } + + private static void SyncToolbarItems(SkiaContentPage platformView, ContentPage page) + { + platformView.ToolbarItems.Clear(); + + foreach (var item in page.ToolbarItems) + { + var skiaItem = new SkiaToolbarItem + { + Text = item.Text ?? "", + Command = item.Command, + Order = item.Order == ToolbarItemOrder.Primary + ? SkiaToolbarItemOrder.Primary + : SkiaToolbarItemOrder.Secondary + }; + + // Load icon if present + if (item.IconImageSource is FileImageSource fileSource) + { + // Icon loading would be async - simplified for now + Console.WriteLine($"[ContentPageHandler] Toolbar item icon: {fileSource.File}"); + } + + platformView.ToolbarItems.Add(skiaItem); + } + + platformView.Invalidate(); + } } diff --git a/Handlers/ShellHandler.cs b/Handlers/ShellHandler.cs index bd7a2b7..71939da 100644 --- a/Handlers/ShellHandler.cs +++ b/Handlers/ShellHandler.cs @@ -4,6 +4,7 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform.Linux.Hosting; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; @@ -13,12 +14,27 @@ namespace Microsoft.Maui.Platform.Linux.Handlers; /// public partial class ShellHandler : ViewHandler { + private bool _isUpdatingFlyoutPresented; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { + [nameof(Shell.FlyoutIsPresented)] = MapFlyoutIsPresented, + [nameof(Shell.FlyoutBehavior)] = MapFlyoutBehavior, + [nameof(Shell.FlyoutWidth)] = MapFlyoutWidth, + [nameof(Shell.FlyoutBackgroundColor)] = MapFlyoutBackgroundColor, + [nameof(Shell.FlyoutBackground)] = MapFlyoutBackground, + [nameof(Shell.BackgroundColor)] = MapBackgroundColor, + [nameof(Shell.FlyoutHeaderBehavior)] = MapFlyoutHeaderBehavior, + [nameof(Shell.FlyoutHeader)] = MapFlyoutHeader, + [nameof(Shell.FlyoutFooter)] = MapFlyoutFooter, + [nameof(Shell.Items)] = MapItems, + [nameof(Shell.CurrentItem)] = MapCurrentItem, + [nameof(Shell.Title)] = MapTitle, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) { + ["GoToAsync"] = MapGoToAsync, }; public ShellHandler() : base(Mapper, CommandMapper) @@ -41,11 +57,21 @@ public partial class ShellHandler : ViewHandler platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; platformView.Navigated += OnNavigated; + // Store reference to MAUI Shell for callbacks + platformView.MauiShell = VirtualView; + + // Set up content renderer + platformView.ContentRenderer = RenderShellContent; + platformView.ColorRefresher = RefreshShellColors; + // Subscribe to Shell navigation events if (VirtualView != null) { VirtualView.Navigating += OnShellNavigating; VirtualView.Navigated += OnShellNavigated; + + // Initial sync of shell items + SyncShellItems(); } } @@ -53,6 +79,9 @@ public partial class ShellHandler : ViewHandler { platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged; platformView.Navigated -= OnNavigated; + platformView.MauiShell = null; + platformView.ContentRenderer = null; + platformView.ColorRefresher = null; if (VirtualView != null) { @@ -65,10 +94,20 @@ public partial class ShellHandler : ViewHandler private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e) { - // Sync flyout state to virtual view + if (VirtualView is null || PlatformView is null || _isUpdatingFlyoutPresented) return; + + try + { + _isUpdatingFlyoutPresented = true; + VirtualView.FlyoutIsPresented = PlatformView.FlyoutIsPresented; + } + finally + { + _isUpdatingFlyoutPresented = false; + } } - private void OnNavigated(object? sender, ShellNavigationEventArgs e) + private void OnNavigated(object? sender, Platform.ShellNavigationEventArgs e) { // Handle platform navigation events } @@ -90,4 +129,289 @@ public partial class ShellHandler : ViewHandler { Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}"); } + + private void SyncShellItems() + { + if (PlatformView is null || VirtualView is null || MauiContext is null) return; + + // Clear existing sections + foreach (var section in PlatformView.Sections.ToList()) + { + PlatformView.RemoveSection(section); + } + + // Add shell items as sections + foreach (var item in VirtualView.Items) + { + if (item is FlyoutItem flyoutItem) + { + var section = new Platform.ShellSection + { + Route = flyoutItem.Route ?? flyoutItem.Title ?? "", + Title = flyoutItem.Title ?? "", + IconPath = flyoutItem.Icon?.ToString() + }; + + // Add shell contents as items + foreach (var shellSection in flyoutItem.Items) + { + foreach (var content in shellSection.Items) + { + var contentItem = new Platform.ShellContent + { + Route = content.Route ?? content.Title ?? "", + Title = content.Title ?? "", + IconPath = content.Icon?.ToString(), + MauiShellContent = content, + Content = RenderShellContent(content) + }; + section.Items.Add(contentItem); + } + } + + PlatformView.AddSection(section); + } + else if (item is ShellItem shellItem) + { + var section = new Platform.ShellSection + { + Route = shellItem.Route ?? shellItem.Title ?? "", + Title = shellItem.Title ?? "", + IconPath = shellItem.Icon?.ToString() + }; + + foreach (var shellSection in shellItem.Items) + { + foreach (var content in shellSection.Items) + { + var contentItem = new Platform.ShellContent + { + Route = content.Route ?? content.Title ?? "", + Title = content.Title ?? "", + IconPath = content.Icon?.ToString(), + MauiShellContent = content, + Content = RenderShellContent(content) + }; + section.Items.Add(contentItem); + } + } + + PlatformView.AddSection(section); + } + } + } + + private SkiaView? RenderShellContent(Microsoft.Maui.Controls.ShellContent content) + { + if (MauiContext is null) return null; + + try + { + var page = content.Content as Page; + if (page == null && content.ContentTemplate != null) + { + page = content.ContentTemplate.CreateContent() as Page; + } + + if (page != null) + { + if (page.Handler == null) + { + page.Handler = page.ToViewHandler(MauiContext); + } + + if (page.Handler?.PlatformView is SkiaView skiaView) + { + return skiaView; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[ShellHandler] Error rendering content: {ex.Message}"); + } + + return null; + } + + private static void RefreshShellColors(SkiaShell platformView, Shell shell) + { + // Sync flyout colors + if (shell.FlyoutBackgroundColor is Color flyoutBgColor) + { + platformView.FlyoutBackgroundColor = flyoutBgColor.ToSKColor(); + } + else if (shell.FlyoutBackground is SolidColorBrush flyoutBrush) + { + platformView.FlyoutBackgroundColor = flyoutBrush.Color.ToSKColor(); + } + + // Sync nav bar colors + if (shell.BackgroundColor is Color bgColor) + { + platformView.NavBarBackgroundColor = bgColor.ToSKColor(); + } + } + + public static void MapFlyoutIsPresented(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null || handler._isUpdatingFlyoutPresented) return; + + try + { + handler._isUpdatingFlyoutPresented = true; + handler.PlatformView.FlyoutIsPresented = shell.FlyoutIsPresented; + } + finally + { + handler._isUpdatingFlyoutPresented = false; + } + } + + public static void MapFlyoutBehavior(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.FlyoutBehavior = shell.FlyoutBehavior switch + { + Microsoft.Maui.FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled, + Microsoft.Maui.FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout, + Microsoft.Maui.FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked, + _ => ShellFlyoutBehavior.Flyout + }; + } + + public static void MapFlyoutWidth(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + handler.PlatformView.FlyoutWidth = (float)shell.FlyoutWidth; + } + + public static void MapFlyoutBackgroundColor(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + + if (shell.FlyoutBackgroundColor is Color color) + { + handler.PlatformView.FlyoutBackgroundColor = color.ToSKColor(); + } + } + + public static void MapFlyoutBackground(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + + if (shell.FlyoutBackground is SolidColorBrush solidBrush) + { + handler.PlatformView.FlyoutBackgroundColor = solidBrush.Color.ToSKColor(); + } + } + + public static void MapBackgroundColor(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + + if (shell.BackgroundColor is Color color) + { + handler.PlatformView.NavBarBackgroundColor = color.ToSKColor(); + } + } + + public static void MapFlyoutHeaderBehavior(ShellHandler handler, Shell shell) + { + // Flyout header behavior - handled by platform view + } + + public static void MapFlyoutHeader(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; + + var header = shell.FlyoutHeader; + if (header == null) + { + handler.PlatformView.FlyoutHeaderView = null; + return; + } + + if (header is View headerView) + { + if (headerView.Handler == null) + { + headerView.Handler = headerView.ToViewHandler(handler.MauiContext); + } + + if (headerView.Handler?.PlatformView is SkiaView skiaHeader) + { + handler.PlatformView.FlyoutHeaderView = skiaHeader; + } + } + } + + public static void MapFlyoutFooter(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; + + var footer = shell.FlyoutFooter; + if (footer == null) + { + handler.PlatformView.FlyoutFooterText = null; + return; + } + + // Simple text footer support + if (footer is Label label) + { + handler.PlatformView.FlyoutFooterText = label.Text; + } + else if (footer is string text) + { + handler.PlatformView.FlyoutFooterText = text; + } + } + + public static void MapItems(ShellHandler handler, Shell shell) + { + handler.SyncShellItems(); + } + + public static void MapCurrentItem(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + + // Sync current item selection + var currentItem = shell.CurrentItem; + if (currentItem != null) + { + // Find matching section index + for (int i = 0; i < handler.PlatformView.Sections.Count; i++) + { + var section = handler.PlatformView.Sections[i]; + if (section.Route == (currentItem.Route ?? currentItem.Title)) + { + handler.PlatformView.NavigateToSection(i); + break; + } + } + } + } + + public static void MapTitle(ShellHandler handler, Shell shell) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Title = shell.Title ?? ""; + } + + public static void MapGoToAsync(ShellHandler handler, Shell shell, object? args) + { + if (handler.PlatformView is null || args is null) return; + + if (args is ShellNavigationState state) + { + handler.PlatformView.GoToAsync(state.Location.ToString()); + } + else if (args is string route) + { + handler.PlatformView.GoToAsync(route); + } + } } diff --git a/Views/SkiaPage.cs b/Views/SkiaPage.cs index ae70923..7c03699 100644 --- a/Views/SkiaPage.cs +++ b/Views/SkiaPage.cs @@ -118,12 +118,26 @@ public class SkiaPage : SkiaView public bool IsBusy { get; set; } + /// + /// Icon image source for this page (used by navigation containers). + /// + public SKBitmap? IconImage { get; set; } + + /// + /// Background image source for this page. + /// + public SKBitmap? BackgroundImage { get; set; } + + // Lifecycle events public event EventHandler? Appearing; public event EventHandler? Disappearing; + public event EventHandler? NavigatedTo; + public event EventHandler? NavigatedFrom; + public event EventHandler? NavigatingFrom; protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - // Draw background + // Draw background color if (BackgroundColor != SKColors.Transparent) { using var bgPaint = new SKPaint @@ -134,6 +148,13 @@ public class SkiaPage : SkiaView canvas.DrawRect(bounds, bgPaint); } + // Draw background image if set + if (BackgroundImage != null) + { + var destRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); + canvas.DrawBitmap(BackgroundImage, destRect); + } + var contentTop = bounds.Top; // Draw navigation bar if visible @@ -254,6 +275,21 @@ public class SkiaPage : SkiaView Disappearing?.Invoke(this, EventArgs.Empty); } + public void OnNavigatedTo() + { + NavigatedTo?.Invoke(this, EventArgs.Empty); + } + + public void OnNavigatedFrom() + { + NavigatedFrom?.Invoke(this, EventArgs.Empty); + } + + public void OnNavigatingFrom() + { + NavigatingFrom?.Invoke(this, EventArgs.Empty); + } + protected override SKSize MeasureOverride(SKSize availableSize) { // Page takes all available space