All checks were successful
CI / Build (Linux) (push) Successful in 21s
Fix critical memory leaks identified in architecture review: Add signal handler disconnection in WebKitNative (load-changed and script-dialog signals now properly cleaned up), implement GTK idle callback cleanup with automatic removal on completion, add dlclose() calls for WebKit library handles, track GTK signal IDs in GtkSkiaSurfaceWidget for proper disposal. Replace empty catch blocks in GestureManager with logged exception handling. Add WebKitNative.Cleanup() and GtkNative.ClearCallbacks() methods for application shutdown.
1326 lines
42 KiB
C#
1326 lines
42 KiB
C#
// 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.Graphics;
|
|
using Microsoft.Maui.Platform.Linux.Services;
|
|
using SkiaSharp;
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
/// <summary>
|
|
/// Shell provides a common navigation experience for MAUI applications.
|
|
/// Supports flyout menu, tabs, and URI-based navigation.
|
|
/// </summary>
|
|
public class SkiaShell : SkiaLayoutView
|
|
{
|
|
#region BindableProperties
|
|
|
|
/// <summary>
|
|
/// Bindable property for FlyoutIsPresented.
|
|
/// </summary>
|
|
public static readonly BindableProperty FlyoutIsPresentedProperty =
|
|
BindableProperty.Create(
|
|
nameof(FlyoutIsPresented),
|
|
typeof(bool),
|
|
typeof(SkiaShell),
|
|
false,
|
|
BindingMode.OneWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutIsPresentedChanged((bool)n));
|
|
|
|
/// <summary>
|
|
/// Bindable property for FlyoutBehavior.
|
|
/// </summary>
|
|
public static readonly BindableProperty FlyoutBehaviorProperty =
|
|
BindableProperty.Create(
|
|
nameof(FlyoutBehavior),
|
|
typeof(ShellFlyoutBehavior),
|
|
typeof(SkiaShell),
|
|
ShellFlyoutBehavior.Flyout,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
|
|
|
|
/// <summary>
|
|
/// Bindable property for FlyoutWidth.
|
|
/// </summary>
|
|
public static readonly BindableProperty FlyoutWidthProperty =
|
|
BindableProperty.Create(
|
|
nameof(FlyoutWidth),
|
|
typeof(float),
|
|
typeof(SkiaShell),
|
|
280f,
|
|
BindingMode.TwoWay,
|
|
coerceValue: (b, v) => Math.Max(100f, (float)v),
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
|
|
|
|
/// <summary>
|
|
/// Bindable property for FlyoutBackgroundColor.
|
|
/// </summary>
|
|
public static readonly BindableProperty FlyoutBackgroundColorProperty =
|
|
BindableProperty.Create(
|
|
nameof(FlyoutBackgroundColor),
|
|
typeof(Color),
|
|
typeof(SkiaShell),
|
|
Colors.White,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutBackgroundColorChanged());
|
|
|
|
/// <summary>
|
|
/// Bindable property for FlyoutTextColor.
|
|
/// </summary>
|
|
public static readonly BindableProperty FlyoutTextColorProperty =
|
|
BindableProperty.Create(
|
|
nameof(FlyoutTextColor),
|
|
typeof(Color),
|
|
typeof(SkiaShell),
|
|
Color.FromRgb(33, 33, 33),
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutTextColorChanged());
|
|
|
|
/// <summary>
|
|
/// Bindable property for NavBarBackgroundColor.
|
|
/// </summary>
|
|
public static readonly BindableProperty NavBarBackgroundColorProperty =
|
|
BindableProperty.Create(
|
|
nameof(NavBarBackgroundColor),
|
|
typeof(Color),
|
|
typeof(SkiaShell),
|
|
Color.FromRgb(33, 150, 243),
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnNavBarBackgroundColorChanged());
|
|
|
|
/// <summary>
|
|
/// Bindable property for NavBarTextColor.
|
|
/// </summary>
|
|
public static readonly BindableProperty NavBarTextColorProperty =
|
|
BindableProperty.Create(
|
|
nameof(NavBarTextColor),
|
|
typeof(Color),
|
|
typeof(SkiaShell),
|
|
Colors.White,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnNavBarTextColorChanged());
|
|
|
|
/// <summary>
|
|
/// Bindable property for NavBarHeight.
|
|
/// </summary>
|
|
public static readonly BindableProperty NavBarHeightProperty =
|
|
BindableProperty.Create(
|
|
nameof(NavBarHeight),
|
|
typeof(float),
|
|
typeof(SkiaShell),
|
|
56f,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for TabBarHeight.
|
|
/// </summary>
|
|
public static readonly BindableProperty TabBarHeightProperty =
|
|
BindableProperty.Create(
|
|
nameof(TabBarHeight),
|
|
typeof(float),
|
|
typeof(SkiaShell),
|
|
56f,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for NavBarIsVisible.
|
|
/// </summary>
|
|
public static readonly BindableProperty NavBarIsVisibleProperty =
|
|
BindableProperty.Create(
|
|
nameof(NavBarIsVisible),
|
|
typeof(bool),
|
|
typeof(SkiaShell),
|
|
true,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for TabBarIsVisible.
|
|
/// </summary>
|
|
public static readonly BindableProperty TabBarIsVisibleProperty =
|
|
BindableProperty.Create(
|
|
nameof(TabBarIsVisible),
|
|
typeof(bool),
|
|
typeof(SkiaShell),
|
|
false,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for ContentPadding.
|
|
/// </summary>
|
|
public static readonly BindableProperty ContentPaddingProperty =
|
|
BindableProperty.Create(
|
|
nameof(ContentPadding),
|
|
typeof(float),
|
|
typeof(SkiaShell),
|
|
0f,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for ContentBackgroundColor.
|
|
/// </summary>
|
|
public static readonly BindableProperty ContentBackgroundColorProperty =
|
|
BindableProperty.Create(
|
|
nameof(ContentBackgroundColor),
|
|
typeof(Color),
|
|
typeof(SkiaShell),
|
|
Color.FromRgb(250, 250, 250),
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).OnContentBackgroundColorChanged());
|
|
|
|
/// <summary>
|
|
/// Bindable property for Title.
|
|
/// </summary>
|
|
public static readonly BindableProperty TitleProperty =
|
|
BindableProperty.Create(
|
|
nameof(Title),
|
|
typeof(string),
|
|
typeof(SkiaShell),
|
|
string.Empty,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
|
|
|
|
#endregion
|
|
|
|
private readonly List<ShellSection> _sections = new();
|
|
private SkiaView? _currentContent;
|
|
private float _flyoutAnimationProgress = 0f;
|
|
private int _selectedSectionIndex = 0;
|
|
private int _selectedItemIndex = 0;
|
|
|
|
// Navigation stack for push/pop navigation
|
|
private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new();
|
|
|
|
private float _flyoutScrollOffset;
|
|
private readonly Dictionary<string, Func<SkiaView?>> _registeredRoutes = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly Dictionary<string, string> _routeTitles = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Internal SKColor fields for rendering
|
|
private SKColor _flyoutBackgroundColorSK = SkiaTheme.BackgroundWhiteSK;
|
|
private SKColor _flyoutTextColorSK = SkiaTheme.TextPrimarySK;
|
|
private SKColor _navBarBackgroundColorSK = SkiaTheme.PrimarySK;
|
|
private SKColor _navBarTextColorSK = SkiaTheme.BackgroundWhiteSK;
|
|
private SKColor _contentBackgroundColorSK = SkiaTheme.Gray50SK;
|
|
|
|
private void OnFlyoutBackgroundColorChanged()
|
|
{
|
|
_flyoutBackgroundColorSK = FlyoutBackgroundColor?.ToSKColor() ?? SkiaTheme.BackgroundWhiteSK;
|
|
Invalidate();
|
|
}
|
|
|
|
private void OnFlyoutTextColorChanged()
|
|
{
|
|
_flyoutTextColorSK = FlyoutTextColor?.ToSKColor() ?? SkiaTheme.TextPrimarySK;
|
|
Invalidate();
|
|
}
|
|
|
|
private void OnNavBarBackgroundColorChanged()
|
|
{
|
|
_navBarBackgroundColorSK = NavBarBackgroundColor?.ToSKColor() ?? SkiaTheme.PrimarySK;
|
|
Invalidate();
|
|
}
|
|
|
|
private void OnNavBarTextColorChanged()
|
|
{
|
|
_navBarTextColorSK = NavBarTextColor?.ToSKColor() ?? SkiaTheme.BackgroundWhiteSK;
|
|
Invalidate();
|
|
}
|
|
|
|
private void OnContentBackgroundColorChanged()
|
|
{
|
|
_contentBackgroundColorSK = ContentBackgroundColor?.ToSKColor() ?? SkiaTheme.Gray50SK;
|
|
Invalidate();
|
|
}
|
|
|
|
private void OnFlyoutIsPresentedChanged(bool newValue)
|
|
{
|
|
_flyoutAnimationProgress = newValue ? 1f : 0f;
|
|
FlyoutIsPresentedChanged?.Invoke(this, EventArgs.Empty);
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the flyout is presented.
|
|
/// </summary>
|
|
public bool FlyoutIsPresented
|
|
{
|
|
get => (bool)GetValue(FlyoutIsPresentedProperty);
|
|
set => SetValue(FlyoutIsPresentedProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the flyout behavior.
|
|
/// </summary>
|
|
public ShellFlyoutBehavior FlyoutBehavior
|
|
{
|
|
get => (ShellFlyoutBehavior)GetValue(FlyoutBehaviorProperty);
|
|
set => SetValue(FlyoutBehaviorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the flyout width.
|
|
/// </summary>
|
|
public float FlyoutWidth
|
|
{
|
|
get => (float)GetValue(FlyoutWidthProperty);
|
|
set => SetValue(FlyoutWidthProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Background color of the flyout.
|
|
/// </summary>
|
|
public Color? FlyoutBackgroundColor
|
|
{
|
|
get => (Color?)GetValue(FlyoutBackgroundColorProperty);
|
|
set => SetValue(FlyoutBackgroundColorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Text color in the flyout.
|
|
/// </summary>
|
|
public Color? FlyoutTextColor
|
|
{
|
|
get => (Color?)GetValue(FlyoutTextColorProperty);
|
|
set => SetValue(FlyoutTextColorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optional header view in the flyout.
|
|
/// </summary>
|
|
public SkiaView? FlyoutHeaderView { get; set; }
|
|
|
|
/// <summary>
|
|
/// Height of the flyout header.
|
|
/// </summary>
|
|
public float FlyoutHeaderHeight { get; set; } = 140f;
|
|
|
|
/// <summary>
|
|
/// Optional footer text in the flyout (fallback if no FlyoutFooterView).
|
|
/// </summary>
|
|
public string? FlyoutFooterText { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional footer view in the flyout.
|
|
/// </summary>
|
|
public SkiaView? FlyoutFooterView { get; set; }
|
|
|
|
/// <summary>
|
|
/// Height of the flyout footer.
|
|
/// </summary>
|
|
public float FlyoutFooterHeight { get; set; } = 40f;
|
|
|
|
/// <summary>
|
|
/// Background color of the navigation bar.
|
|
/// </summary>
|
|
public Color? NavBarBackgroundColor
|
|
{
|
|
get => (Color?)GetValue(NavBarBackgroundColorProperty);
|
|
set => SetValue(NavBarBackgroundColorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Text color of the navigation bar title.
|
|
/// </summary>
|
|
public Color? NavBarTextColor
|
|
{
|
|
get => (Color?)GetValue(NavBarTextColorProperty);
|
|
set => SetValue(NavBarTextColorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Height of the navigation bar.
|
|
/// </summary>
|
|
public float NavBarHeight
|
|
{
|
|
get => (float)GetValue(NavBarHeightProperty);
|
|
set => SetValue(NavBarHeightProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Height of the tab bar (when using bottom tabs).
|
|
/// </summary>
|
|
public float TabBarHeight
|
|
{
|
|
get => (float)GetValue(TabBarHeightProperty);
|
|
set => SetValue(TabBarHeightProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the navigation bar is visible.
|
|
/// </summary>
|
|
public bool NavBarIsVisible
|
|
{
|
|
get => (bool)GetValue(NavBarIsVisibleProperty);
|
|
set => SetValue(NavBarIsVisibleProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the tab bar is visible.
|
|
/// </summary>
|
|
public bool TabBarIsVisible
|
|
{
|
|
get => (bool)GetValue(TabBarIsVisibleProperty);
|
|
set => SetValue(TabBarIsVisibleProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the padding applied to page content.
|
|
/// </summary>
|
|
public float ContentPadding
|
|
{
|
|
get => (float)GetValue(ContentPaddingProperty);
|
|
set => SetValue(ContentPaddingProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Background color of the content area.
|
|
/// </summary>
|
|
public Color? ContentBackgroundColor
|
|
{
|
|
get => (Color?)GetValue(ContentBackgroundColorProperty);
|
|
set => SetValue(ContentBackgroundColorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current title displayed in the navigation bar.
|
|
/// </summary>
|
|
public string Title
|
|
{
|
|
get => (string)GetValue(TitleProperty);
|
|
set => SetValue(TitleProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The sections in this shell.
|
|
/// </summary>
|
|
public IReadOnlyList<ShellSection> Sections => _sections;
|
|
|
|
/// <summary>
|
|
/// Gets the currently selected section index.
|
|
/// </summary>
|
|
public int CurrentSectionIndex => _selectedSectionIndex;
|
|
|
|
/// <summary>
|
|
/// Reference to the MAUI Shell this view represents.
|
|
/// </summary>
|
|
public Shell? MauiShell { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback to render content from a ShellContent.
|
|
/// </summary>
|
|
public Func<Microsoft.Maui.Controls.ShellContent, SkiaView?>? ContentRenderer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback to refresh shell colors.
|
|
/// </summary>
|
|
public Action<SkiaShell, Shell>? ColorRefresher { get; set; }
|
|
|
|
/// <summary>
|
|
/// Event raised when FlyoutIsPresented changes.
|
|
/// </summary>
|
|
public event EventHandler? FlyoutIsPresentedChanged;
|
|
|
|
/// <summary>
|
|
/// Event raised when navigation occurs.
|
|
/// </summary>
|
|
public event EventHandler<ShellNavigationEventArgs>? Navigated;
|
|
|
|
/// <summary>
|
|
/// Adds a section to the shell.
|
|
/// </summary>
|
|
public void AddSection(ShellSection section)
|
|
{
|
|
_sections.Add(section);
|
|
|
|
if (_sections.Count == 1)
|
|
{
|
|
NavigateToSection(0, 0);
|
|
}
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a section from the shell.
|
|
/// </summary>
|
|
public void RemoveSection(ShellSection section)
|
|
{
|
|
_sections.Remove(section);
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Navigates to a specific section and item.
|
|
/// </summary>
|
|
public void NavigateToSection(int sectionIndex, int itemIndex = 0)
|
|
{
|
|
if (sectionIndex < 0 || sectionIndex >= _sections.Count) return;
|
|
|
|
var section = _sections[sectionIndex];
|
|
if (itemIndex < 0 || itemIndex >= section.Items.Count) return;
|
|
|
|
// Clear navigation stack when navigating to a new section
|
|
_navigationStack.Clear();
|
|
|
|
_selectedSectionIndex = sectionIndex;
|
|
_selectedItemIndex = itemIndex;
|
|
|
|
var item = section.Items[itemIndex];
|
|
SetCurrentContent(item.Content);
|
|
Title = item.Title;
|
|
|
|
Navigated?.Invoke(this, new ShellNavigationEventArgs(section, item));
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the shell theme and re-renders all pages.
|
|
/// </summary>
|
|
public void RefreshTheme()
|
|
{
|
|
DiagnosticLog.Debug("SkiaShell", "RefreshTheme called - refreshing all pages");
|
|
if (MauiShell != null && ColorRefresher != null)
|
|
{
|
|
DiagnosticLog.Debug("SkiaShell", "Refreshing shell colors");
|
|
ColorRefresher(this, MauiShell);
|
|
}
|
|
if (ContentRenderer != null)
|
|
{
|
|
foreach (var section in _sections)
|
|
{
|
|
foreach (var item in section.Items)
|
|
{
|
|
if (item.MauiShellContent != null)
|
|
{
|
|
DiagnosticLog.Debug("SkiaShell", "Re-rendering: " + item.Title);
|
|
var skiaView = ContentRenderer(item.MauiShellContent);
|
|
if (skiaView != null)
|
|
{
|
|
item.Content = skiaView;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Only update current content if there are no pushed pages on the navigation stack
|
|
// Pushed pages are handled separately by LinuxApplication.RefreshViewTheme
|
|
if (_navigationStack.Count == 0 && _selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count)
|
|
{
|
|
var section = _sections[_selectedSectionIndex];
|
|
if (_selectedItemIndex >= 0 && _selectedItemIndex < section.Items.Count)
|
|
{
|
|
var item = section.Items[_selectedItemIndex];
|
|
SetCurrentContent(item.Content);
|
|
}
|
|
}
|
|
InvalidateMeasure();
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Navigates using a URI route.
|
|
/// </summary>
|
|
public void GoToAsync(string route)
|
|
{
|
|
GoToAsync(route, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Navigates using a URI route with parameters.
|
|
/// </summary>
|
|
public void GoToAsync(string route, IDictionary<string, object>? parameters)
|
|
{
|
|
if (string.IsNullOrEmpty(route)) return;
|
|
|
|
string routePath = route;
|
|
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
|
int queryIndex = route.IndexOf('?');
|
|
if (queryIndex >= 0)
|
|
{
|
|
routePath = route.Substring(0, queryIndex);
|
|
queryParams = ParseQueryString(route.Substring(queryIndex + 1));
|
|
}
|
|
|
|
Dictionary<string, object> allParams = new Dictionary<string, object>();
|
|
foreach (var kvp in queryParams)
|
|
{
|
|
allParams[kvp.Key] = kvp.Value;
|
|
}
|
|
if (parameters != null)
|
|
{
|
|
foreach (var kvp in parameters)
|
|
{
|
|
allParams[kvp.Key] = kvp.Value;
|
|
}
|
|
}
|
|
|
|
var parts = routePath.TrimStart('/').Split('/');
|
|
if (parts.Length == 0) return;
|
|
|
|
// Check registered routes first
|
|
if (_registeredRoutes.TryGetValue(routePath.TrimStart('/'), out Func<SkiaView?>? factory))
|
|
{
|
|
var view = factory();
|
|
if (view != null)
|
|
{
|
|
ApplyQueryParameters(view, allParams);
|
|
PushAsync(view, GetRouteTitle(routePath.TrimStart('/')));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find matching section
|
|
for (int i = 0; i < _sections.Count; i++)
|
|
{
|
|
var section = _sections[i];
|
|
if (!section.Route.Equals(parts[0], StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (parts.Length > 1)
|
|
{
|
|
// Find matching item
|
|
for (int j = 0; j < section.Items.Count; j++)
|
|
{
|
|
if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NavigateToSection(i, j);
|
|
if (section.Items[j].Content != null && allParams.Count > 0)
|
|
{
|
|
ApplyQueryParameters(section.Items[j].Content!, allParams);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
NavigateToSection(i);
|
|
if (section.Items.Count > 0 && section.Items[0].Content != null && allParams.Count > 0)
|
|
{
|
|
ApplyQueryParameters(section.Items[0].Content!, allParams);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, string> ParseQueryString(string queryString)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (string.IsNullOrEmpty(queryString)) return result;
|
|
|
|
var pairs = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (var pair in pairs)
|
|
{
|
|
var parts = pair.Split('=', 2);
|
|
if (parts.Length == 2)
|
|
{
|
|
result[Uri.UnescapeDataString(parts[0])] = Uri.UnescapeDataString(parts[1]);
|
|
}
|
|
else if (parts.Length == 1)
|
|
{
|
|
result[Uri.UnescapeDataString(parts[0])] = string.Empty;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static void ApplyQueryParameters(SkiaView content, IDictionary<string, object> parameters)
|
|
{
|
|
if (parameters.Count == 0) return;
|
|
|
|
if (content is ISkiaQueryAttributable attributable)
|
|
{
|
|
attributable.ApplyQueryAttributes(parameters);
|
|
}
|
|
|
|
var type = content.GetType();
|
|
foreach (var param in parameters)
|
|
{
|
|
var prop = type.GetProperty(param.Key, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
|
if (prop != null && prop.CanWrite)
|
|
{
|
|
try
|
|
{
|
|
var value = Convert.ChangeType(param.Value, prop.PropertyType);
|
|
prop.SetValue(content, value);
|
|
}
|
|
catch (Exception ex) { DiagnosticLog.Debug("SkiaShell", "Parameter type conversion failed", ex); }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a route with a content factory.
|
|
/// </summary>
|
|
public void RegisterRoute(string route, Func<SkiaView?> contentFactory, string? title = null)
|
|
{
|
|
var key = route.TrimStart('/');
|
|
_registeredRoutes[key] = contentFactory;
|
|
if (!string.IsNullOrEmpty(title))
|
|
{
|
|
_routeTitles[key] = title;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a route.
|
|
/// </summary>
|
|
public void UnregisterRoute(string route)
|
|
{
|
|
var key = route.TrimStart('/');
|
|
_registeredRoutes.Remove(key);
|
|
_routeTitles.Remove(key);
|
|
}
|
|
|
|
private string GetRouteTitle(string route)
|
|
{
|
|
if (_routeTitles.TryGetValue(route, out string? title))
|
|
{
|
|
return title;
|
|
}
|
|
return route.Split('/').LastOrDefault() ?? route;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether there are pages on the navigation stack.
|
|
/// </summary>
|
|
public bool CanGoBack => _navigationStack.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Gets the current navigation stack depth.
|
|
/// </summary>
|
|
public int NavigationStackDepth => _navigationStack.Count;
|
|
|
|
/// <summary>
|
|
/// Pushes a new page onto the navigation stack.
|
|
/// </summary>
|
|
public void PushAsync(SkiaView page, string title)
|
|
{
|
|
// Save current content to stack
|
|
if (_currentContent != null)
|
|
{
|
|
_navigationStack.Push((_currentContent, Title));
|
|
}
|
|
|
|
// Set new content
|
|
SetCurrentContent(page);
|
|
Title = title;
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pops the current page from the navigation stack.
|
|
/// </summary>
|
|
public bool PopAsync()
|
|
{
|
|
if (_navigationStack.Count == 0) return false;
|
|
|
|
var (previousContent, previousTitle) = _navigationStack.Pop();
|
|
SetCurrentContent(previousContent);
|
|
Title = previousTitle;
|
|
Invalidate();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pops all pages from the navigation stack, returning to the root.
|
|
/// </summary>
|
|
public void PopToRootAsync()
|
|
{
|
|
if (_navigationStack.Count == 0) return;
|
|
|
|
// Get the root content
|
|
(SkiaView Content, string Title) root = default;
|
|
while (_navigationStack.Count > 0)
|
|
{
|
|
root = _navigationStack.Pop();
|
|
}
|
|
|
|
SetCurrentContent(root.Content);
|
|
Title = root.Title ?? string.Empty;
|
|
Invalidate();
|
|
}
|
|
|
|
private void SetCurrentContent(SkiaView? content)
|
|
{
|
|
if (_currentContent != null)
|
|
{
|
|
RemoveChild(_currentContent);
|
|
}
|
|
|
|
_currentContent = content;
|
|
|
|
if (_currentContent != null)
|
|
{
|
|
AddChild(_currentContent);
|
|
}
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
// Measure current content with padding accounted for (consistent with ArrangeOverride)
|
|
if (_currentContent != null)
|
|
{
|
|
float contentTop = NavBarIsVisible ? NavBarHeight : 0;
|
|
float contentBottom = TabBarIsVisible ? TabBarHeight : 0;
|
|
var contentSize = new Size(
|
|
availableSize.Width - Padding.Left - Padding.Right,
|
|
availableSize.Height - contentTop - contentBottom - Padding.Top - Padding.Bottom);
|
|
_currentContent.Measure(contentSize);
|
|
}
|
|
|
|
return availableSize;
|
|
}
|
|
|
|
protected override Rect ArrangeOverride(Rect bounds)
|
|
{
|
|
DiagnosticLog.Debug("SkiaShell", $"ArrangeOverride - bounds={bounds}");
|
|
|
|
// Arrange current content with padding
|
|
if (_currentContent != null)
|
|
{
|
|
float contentTop = (float)bounds.Top + (NavBarIsVisible ? NavBarHeight : 0) + ContentPadding;
|
|
float contentBottom = (float)bounds.Bottom - (TabBarIsVisible ? TabBarHeight : 0) - ContentPadding;
|
|
var contentBounds = new Rect(
|
|
bounds.Left + ContentPadding,
|
|
contentTop,
|
|
bounds.Width - ContentPadding * 2,
|
|
contentBottom - contentTop);
|
|
DiagnosticLog.Debug("SkiaShell", $"Arranging content with bounds={contentBounds}, padding={ContentPadding}");
|
|
_currentContent.Arrange(contentBounds);
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
{
|
|
canvas.Save();
|
|
canvas.ClipRect(bounds);
|
|
|
|
// Draw content
|
|
_currentContent?.Draw(canvas);
|
|
|
|
// Draw navigation bar
|
|
if (NavBarIsVisible)
|
|
{
|
|
DrawNavBar(canvas, bounds);
|
|
}
|
|
|
|
// Draw tab bar
|
|
if (TabBarIsVisible)
|
|
{
|
|
DrawTabBar(canvas, bounds);
|
|
}
|
|
|
|
// Draw flyout overlay and panel
|
|
if (_flyoutAnimationProgress > 0)
|
|
{
|
|
DrawFlyout(canvas, bounds);
|
|
}
|
|
|
|
canvas.Restore();
|
|
}
|
|
|
|
private void DrawNavBar(SKCanvas canvas, SKRect bounds)
|
|
{
|
|
var navBarBounds = new SKRect(
|
|
bounds.Left,
|
|
bounds.Top,
|
|
bounds.Right,
|
|
bounds.Top + NavBarHeight);
|
|
|
|
// Draw background
|
|
using var bgPaint = new SKPaint
|
|
{
|
|
Color = _navBarBackgroundColorSK,
|
|
Style = SKPaintStyle.Fill,
|
|
IsAntialias = true
|
|
};
|
|
canvas.DrawRect(navBarBounds, bgPaint);
|
|
|
|
// Draw nav icon (back arrow if can go back, else hamburger menu if flyout enabled)
|
|
using var iconPaint = new SKPaint
|
|
{
|
|
Color = _navBarTextColorSK,
|
|
Style = SKPaintStyle.Stroke,
|
|
StrokeWidth = 2,
|
|
StrokeCap = SKStrokeCap.Round,
|
|
IsAntialias = true
|
|
};
|
|
|
|
float iconLeft = navBarBounds.Left + 16;
|
|
float iconCenter = navBarBounds.MidY;
|
|
|
|
if (CanGoBack)
|
|
{
|
|
// Draw iOS-style back chevron "<"
|
|
using var chevronPaint = new SKPaint
|
|
{
|
|
Color = _navBarTextColorSK,
|
|
Style = SKPaintStyle.Stroke,
|
|
StrokeWidth = 2.5f,
|
|
StrokeCap = SKStrokeCap.Round,
|
|
StrokeJoin = SKStrokeJoin.Round,
|
|
IsAntialias = true
|
|
};
|
|
|
|
// Clean chevron pointing left
|
|
float chevronX = iconLeft + 6;
|
|
float chevronSize = 10;
|
|
canvas.DrawLine(chevronX + chevronSize, iconCenter - chevronSize, chevronX, iconCenter, chevronPaint);
|
|
canvas.DrawLine(chevronX, iconCenter, chevronX + chevronSize, iconCenter + chevronSize, chevronPaint);
|
|
}
|
|
else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout)
|
|
{
|
|
// Draw hamburger menu icon
|
|
canvas.DrawLine(iconLeft, iconCenter - 8, iconLeft + 18, iconCenter - 8, iconPaint);
|
|
canvas.DrawLine(iconLeft, iconCenter, iconLeft + 18, iconCenter, iconPaint);
|
|
canvas.DrawLine(iconLeft, iconCenter + 8, iconLeft + 18, iconCenter + 8, iconPaint);
|
|
}
|
|
|
|
// Draw title
|
|
using var titlePaint = new SKPaint
|
|
{
|
|
Color = _navBarTextColorSK,
|
|
TextSize = 20f,
|
|
IsAntialias = true,
|
|
FakeBoldText = true
|
|
};
|
|
|
|
float titleX = (CanGoBack || FlyoutBehavior == ShellFlyoutBehavior.Flyout) ? navBarBounds.Left + 56 : navBarBounds.Left + 16;
|
|
float titleY = navBarBounds.MidY + 6;
|
|
canvas.DrawText(Title, titleX, titleY, titlePaint);
|
|
}
|
|
|
|
private void DrawTabBar(SKCanvas canvas, SKRect bounds)
|
|
{
|
|
if (_selectedSectionIndex < 0 || _selectedSectionIndex >= _sections.Count) return;
|
|
|
|
var section = _sections[_selectedSectionIndex];
|
|
if (section.Items.Count <= 1) return;
|
|
|
|
var tabBarBounds = new SKRect(
|
|
bounds.Left,
|
|
bounds.Bottom - TabBarHeight,
|
|
bounds.Right,
|
|
bounds.Bottom);
|
|
|
|
// Draw background
|
|
using var bgPaint = new SKPaint
|
|
{
|
|
Color = SkiaTheme.BackgroundWhiteSK,
|
|
Style = SKPaintStyle.Fill,
|
|
IsAntialias = true
|
|
};
|
|
canvas.DrawRect(tabBarBounds, bgPaint);
|
|
|
|
// Draw top border
|
|
using var borderPaint = new SKPaint
|
|
{
|
|
Color = SkiaTheme.Gray300SK,
|
|
Style = SKPaintStyle.Stroke,
|
|
StrokeWidth = 1
|
|
};
|
|
canvas.DrawLine(tabBarBounds.Left, tabBarBounds.Top, tabBarBounds.Right, tabBarBounds.Top, borderPaint);
|
|
|
|
// Draw tabs
|
|
float tabWidth = tabBarBounds.Width / section.Items.Count;
|
|
|
|
using var textPaint = new SKPaint
|
|
{
|
|
TextSize = 12f,
|
|
IsAntialias = true
|
|
};
|
|
|
|
for (int i = 0; i < section.Items.Count; i++)
|
|
{
|
|
var item = section.Items[i];
|
|
bool isSelected = i == _selectedItemIndex;
|
|
|
|
textPaint.Color = isSelected ? _navBarBackgroundColorSK : SkiaTheme.TextTertiarySK;
|
|
|
|
var textBounds = new SKRect();
|
|
textPaint.MeasureText(item.Title, ref textBounds);
|
|
|
|
float textX = tabBarBounds.Left + i * tabWidth + tabWidth / 2 - textBounds.MidX;
|
|
float textY = tabBarBounds.MidY - textBounds.MidY;
|
|
|
|
canvas.DrawText(item.Title, textX, textY, textPaint);
|
|
}
|
|
}
|
|
|
|
private void DrawFlyout(SKCanvas canvas, SKRect bounds)
|
|
{
|
|
// Draw scrim
|
|
using var scrimPaint = new SKPaint
|
|
{
|
|
Color = SkiaTheme.Shadow40SK.WithAlpha((byte)(100 * _flyoutAnimationProgress)),
|
|
Style = SKPaintStyle.Fill
|
|
};
|
|
canvas.DrawRect(bounds, scrimPaint);
|
|
|
|
// Draw flyout panel
|
|
float flyoutX = bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress);
|
|
var flyoutBounds = new SKRect(
|
|
flyoutX,
|
|
bounds.Top,
|
|
flyoutX + FlyoutWidth,
|
|
bounds.Bottom);
|
|
|
|
using var flyoutPaint = new SKPaint
|
|
{
|
|
Color = _flyoutBackgroundColorSK,
|
|
Style = SKPaintStyle.Fill,
|
|
IsAntialias = true
|
|
};
|
|
canvas.DrawRect(flyoutBounds, flyoutPaint);
|
|
|
|
// Calculate header and footer heights
|
|
float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f;
|
|
float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight :
|
|
(!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f);
|
|
|
|
// Draw flyout header if present
|
|
if (FlyoutHeaderView != null)
|
|
{
|
|
var headerBounds = new SKRect(flyoutBounds.Left, flyoutBounds.Top, flyoutBounds.Right, flyoutBounds.Top + headerHeight);
|
|
FlyoutHeaderView.Measure(new Size(headerBounds.Width, headerBounds.Height));
|
|
FlyoutHeaderView.Arrange(new Rect(headerBounds.Left, headerBounds.Top, headerBounds.Width, headerBounds.Height));
|
|
FlyoutHeaderView.Draw(canvas);
|
|
}
|
|
|
|
// Draw flyout items with scrolling support
|
|
float itemHeight = 48f;
|
|
float itemsAreaTop = flyoutBounds.Top + headerHeight;
|
|
float itemsAreaBottom = flyoutBounds.Bottom - footerHeight;
|
|
|
|
// Clip to items area (between header and footer)
|
|
canvas.Save();
|
|
canvas.ClipRect(new SKRect(flyoutBounds.Left, itemsAreaTop, flyoutBounds.Right, itemsAreaBottom));
|
|
|
|
// Apply scroll offset
|
|
float itemY = itemsAreaTop - _flyoutScrollOffset;
|
|
|
|
using var itemTextPaint = new SKPaint
|
|
{
|
|
TextSize = 14f,
|
|
IsAntialias = true
|
|
};
|
|
|
|
for (int i = 0; i < _sections.Count; i++)
|
|
{
|
|
var section = _sections[i];
|
|
bool isSelected = i == _selectedSectionIndex;
|
|
|
|
// Skip items that are scrolled above the visible area
|
|
if (itemY + itemHeight < itemsAreaTop)
|
|
{
|
|
itemY += itemHeight;
|
|
continue;
|
|
}
|
|
|
|
// Stop if we're below the visible area
|
|
if (itemY > itemsAreaBottom)
|
|
break;
|
|
|
|
// Draw selection background
|
|
if (isSelected)
|
|
{
|
|
using var selectionPaint = new SKPaint
|
|
{
|
|
Color = SkiaTheme.PrimarySelectionSK.WithAlpha(30),
|
|
Style = SKPaintStyle.Fill
|
|
};
|
|
var selectionRect = new SKRect(flyoutBounds.Left, itemY, flyoutBounds.Right, itemY + itemHeight);
|
|
canvas.DrawRect(selectionRect, selectionPaint);
|
|
}
|
|
|
|
itemTextPaint.Color = isSelected ? _navBarBackgroundColorSK : _flyoutTextColorSK;
|
|
canvas.DrawText(section.Title, flyoutBounds.Left + 16, itemY + 30, itemTextPaint);
|
|
|
|
itemY += itemHeight;
|
|
}
|
|
|
|
canvas.Restore();
|
|
|
|
// Draw flyout footer
|
|
if (FlyoutFooterView != null)
|
|
{
|
|
var footerBounds = new SKRect(flyoutBounds.Left, flyoutBounds.Bottom - footerHeight, flyoutBounds.Right, flyoutBounds.Bottom);
|
|
FlyoutFooterView.Measure(new Size(footerBounds.Width, footerBounds.Height));
|
|
FlyoutFooterView.Arrange(new Rect(footerBounds.Left, footerBounds.Top, footerBounds.Width, footerBounds.Height));
|
|
FlyoutFooterView.Draw(canvas);
|
|
}
|
|
else if (!string.IsNullOrEmpty(FlyoutFooterText))
|
|
{
|
|
// Fallback: draw simple text footer
|
|
using var footerPaint = new SKPaint
|
|
{
|
|
TextSize = 12f,
|
|
Color = _flyoutTextColorSK.WithAlpha(180),
|
|
IsAntialias = true
|
|
};
|
|
var footerY = flyoutBounds.Bottom - footerHeight / 2 + 4;
|
|
canvas.DrawText(FlyoutFooterText, flyoutBounds.Left + 16, footerY, footerPaint);
|
|
}
|
|
}
|
|
|
|
public override SkiaView? HitTest(float x, float y)
|
|
{
|
|
if (!IsVisible || !Bounds.Contains(x, y)) return null;
|
|
|
|
// Check flyout area
|
|
if (_flyoutAnimationProgress > 0)
|
|
{
|
|
float flyoutX = (float)Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress);
|
|
var flyoutBounds = new SKRect(flyoutX, (float)Bounds.Top, flyoutX + FlyoutWidth, (float)Bounds.Bottom);
|
|
|
|
if (flyoutBounds.Contains(x, y))
|
|
{
|
|
return this; // Flyout handles its own hits
|
|
}
|
|
|
|
// Tap on scrim closes flyout
|
|
if (FlyoutIsPresented)
|
|
{
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// Check nav bar
|
|
if (NavBarIsVisible && y < (float)Bounds.Top + NavBarHeight)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// Check tab bar
|
|
if (TabBarIsVisible && y > (float)Bounds.Bottom - TabBarHeight)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// Check content
|
|
if (_currentContent != null)
|
|
{
|
|
var hit = _currentContent.HitTest(x, y);
|
|
if (hit != null) return hit;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
{
|
|
if (!IsEnabled) return;
|
|
|
|
// Check flyout tap
|
|
if (_flyoutAnimationProgress > 0)
|
|
{
|
|
float flyoutX = (float)Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress);
|
|
var flyoutBounds = new SKRect(flyoutX, (float)Bounds.Top, flyoutX + FlyoutWidth, (float)Bounds.Bottom);
|
|
|
|
if (flyoutBounds.Contains(e.X, e.Y))
|
|
{
|
|
// Calculate header and footer heights
|
|
float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f;
|
|
float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight :
|
|
(!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f);
|
|
|
|
float itemsAreaTop = flyoutBounds.Top + headerHeight;
|
|
float itemsAreaBottom = flyoutBounds.Bottom - footerHeight;
|
|
|
|
// Only check items if tap is in items area
|
|
if (e.Y >= itemsAreaTop && e.Y < itemsAreaBottom)
|
|
{
|
|
// Apply scroll offset to find which item was tapped
|
|
float itemY = itemsAreaTop - _flyoutScrollOffset;
|
|
float itemHeight = 48f;
|
|
|
|
for (int i = 0; i < _sections.Count; i++)
|
|
{
|
|
if (e.Y >= itemY && e.Y < itemY + itemHeight)
|
|
{
|
|
NavigateToSection(i, 0);
|
|
FlyoutIsPresented = false;
|
|
_flyoutScrollOffset = 0; // Reset scroll when closing
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
itemY += itemHeight;
|
|
}
|
|
}
|
|
}
|
|
else if (FlyoutIsPresented)
|
|
{
|
|
// Tap on scrim
|
|
FlyoutIsPresented = false;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check nav bar icon tap (back button or hamburger menu)
|
|
if (NavBarIsVisible && e.Y < Bounds.Top + NavBarHeight && e.X < 56)
|
|
{
|
|
if (CanGoBack)
|
|
{
|
|
// Back button pressed
|
|
PopAsync();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout)
|
|
{
|
|
// Hamburger menu pressed
|
|
FlyoutIsPresented = !FlyoutIsPresented;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check tab bar tap
|
|
if (TabBarIsVisible && e.Y > (float)Bounds.Bottom - TabBarHeight)
|
|
{
|
|
if (_selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count)
|
|
{
|
|
var section = _sections[_selectedSectionIndex];
|
|
float tabWidth = (float)Bounds.Width / section.Items.Count;
|
|
int tappedIndex = (int)((e.X - (float)Bounds.Left) / tabWidth);
|
|
tappedIndex = Math.Clamp(tappedIndex, 0, section.Items.Count - 1);
|
|
|
|
if (tappedIndex != _selectedItemIndex)
|
|
{
|
|
NavigateToSection(_selectedSectionIndex, tappedIndex);
|
|
}
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
base.OnPointerPressed(e);
|
|
}
|
|
|
|
public override void OnScroll(ScrollEventArgs e)
|
|
{
|
|
if (FlyoutIsPresented && _flyoutAnimationProgress > 0)
|
|
{
|
|
float flyoutX = (float)Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress);
|
|
var flyoutBounds = new SKRect(flyoutX, (float)Bounds.Top, flyoutX + FlyoutWidth, (float)Bounds.Bottom);
|
|
|
|
if (flyoutBounds.Contains(e.X, e.Y))
|
|
{
|
|
float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f;
|
|
float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight :
|
|
(!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f);
|
|
float itemHeight = 48f;
|
|
float totalItemsHeight = _sections.Count * itemHeight;
|
|
float viewableHeight = flyoutBounds.Height - headerHeight - footerHeight;
|
|
float maxScroll = Math.Max(0f, totalItemsHeight - viewableHeight);
|
|
|
|
_flyoutScrollOffset += e.DeltaY * 30f;
|
|
_flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, maxScroll));
|
|
Invalidate();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
base.OnScroll(e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shell flyout behavior options.
|
|
/// </summary>
|
|
public enum ShellFlyoutBehavior
|
|
{
|
|
/// <summary>
|
|
/// No flyout menu.
|
|
/// </summary>
|
|
Disabled,
|
|
|
|
/// <summary>
|
|
/// Flyout slides over content.
|
|
/// </summary>
|
|
Flyout,
|
|
|
|
/// <summary>
|
|
/// Flyout is always visible (side-by-side layout).
|
|
/// </summary>
|
|
Locked
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a section in the shell (typically shown in flyout).
|
|
/// </summary>
|
|
public class ShellSection
|
|
{
|
|
/// <summary>
|
|
/// The route identifier for this section.
|
|
/// </summary>
|
|
public string Route { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The display title.
|
|
/// </summary>
|
|
public string Title { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Optional icon path.
|
|
/// </summary>
|
|
public string? IconPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// Items in this section.
|
|
/// </summary>
|
|
public List<ShellContent> Items { get; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents content within a shell section.
|
|
/// </summary>
|
|
public class ShellContent
|
|
{
|
|
/// <summary>
|
|
/// The route identifier for this content.
|
|
/// </summary>
|
|
public string Route { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The display title.
|
|
/// </summary>
|
|
public string Title { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Optional icon path.
|
|
/// </summary>
|
|
public string? IconPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// The content view.
|
|
/// </summary>
|
|
public SkiaView? Content { get; set; }
|
|
|
|
/// <summary>
|
|
/// Reference to the MAUI ShellContent this represents.
|
|
/// </summary>
|
|
public Microsoft.Maui.Controls.ShellContent? MauiShellContent { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event args for shell navigation events.
|
|
/// </summary>
|
|
public class ShellNavigationEventArgs : EventArgs
|
|
{
|
|
public ShellSection Section { get; }
|
|
public ShellContent Content { get; }
|
|
|
|
public ShellNavigationEventArgs(ShellSection section, ShellContent content)
|
|
{
|
|
Section = section;
|
|
Content = content;
|
|
}
|
|
}
|