Preview 3: Complete control implementation with XAML data binding

Major milestone adding full control functionality:

Controls Enhanced:
- Entry/Editor: Full keyboard input, cursor navigation, selection, clipboard
- CollectionView: Data binding, selection highlighting, scrolling
- CheckBox/Switch/Slider: Interactive state management
- Picker/DatePicker/TimePicker: Dropdown selection with popup overlays
- ProgressBar/ActivityIndicator: Animated progress display
- Button: Press/release visual states
- Border/Frame: Rounded corners, stroke styling
- Label: Text wrapping, alignment, decorations
- Grid/StackLayout: Margin and padding support

Features Added:
- DisplayAlert dialogs with button actions
- NavigationPage with toolbar and back navigation
- Shell with flyout menu navigation
- XAML value converters for data binding
- Margin support in all layout containers
- Popup overlay system for pickers

New Samples:
- TodoApp: Full CRUD task manager with NavigationPage
- ShellDemo: Comprehensive control showcase

Removed:
- ControlGallery (replaced by ShellDemo)
- LinuxDemo (replaced by TodoApp)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
logikonline
2025-12-21 13:26:56 -05:00
parent f945d2a537
commit 1d55ac672a
142 changed files with 38925 additions and 4201 deletions

View File

@@ -15,6 +15,8 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
[nameof(IActivityIndicator.Color)] = MapColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
@@ -40,4 +42,22 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
handler.PlatformView.IsEnabled = activityIndicator.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -0,0 +1,137 @@
// 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.Handlers;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform.Linux.Hosting;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for MAUI Application on Linux.
/// Bridges the MAUI Application lifecycle with LinuxApplication.
/// </summary>
public partial class ApplicationHandler : ElementHandler<IApplication, LinuxApplicationContext>
{
public static IPropertyMapper<IApplication, ApplicationHandler> Mapper =
new PropertyMapper<IApplication, ApplicationHandler>(ElementHandler.ElementMapper)
{
};
public static CommandMapper<IApplication, ApplicationHandler> CommandMapper =
new(ElementHandler.ElementCommandMapper)
{
[nameof(IApplication.OpenWindow)] = MapOpenWindow,
[nameof(IApplication.CloseWindow)] = MapCloseWindow,
};
public ApplicationHandler() : base(Mapper, CommandMapper)
{
}
public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override LinuxApplicationContext CreatePlatformElement()
{
return new LinuxApplicationContext();
}
protected override void ConnectHandler(LinuxApplicationContext platformView)
{
base.ConnectHandler(platformView);
platformView.Application = VirtualView;
}
protected override void DisconnectHandler(LinuxApplicationContext platformView)
{
platformView.Application = null;
base.DisconnectHandler(platformView);
}
public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args)
{
if (args is IWindow window)
{
handler.PlatformView?.OpenWindow(window);
}
}
public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args)
{
if (args is IWindow window)
{
handler.PlatformView?.CloseWindow(window);
}
}
}
/// <summary>
/// Platform context for the MAUI Application on Linux.
/// Manages windows and the application lifecycle.
/// </summary>
public class LinuxApplicationContext
{
private readonly List<IWindow> _windows = new();
private IApplication? _application;
/// <summary>
/// Gets or sets the MAUI Application.
/// </summary>
public IApplication? Application
{
get => _application;
set
{
_application = value;
if (_application != null)
{
// Initialize windows from the application
foreach (var window in _application.Windows)
{
if (!_windows.Contains(window))
{
_windows.Add(window);
}
}
}
}
}
/// <summary>
/// Gets the list of open windows.
/// </summary>
public IReadOnlyList<IWindow> Windows => _windows;
/// <summary>
/// Opens a window and creates its handler.
/// </summary>
public void OpenWindow(IWindow window)
{
if (!_windows.Contains(window))
{
_windows.Add(window);
}
}
/// <summary>
/// Closes a window and cleans up its handler.
/// </summary>
public void CloseWindow(IWindow window)
{
_windows.Remove(window);
if (_windows.Count == 0)
{
// Last window closed, stop the application
LinuxApplication.Current?.MainWindow?.Stop();
}
}
/// <summary>
/// Gets the main window of the application.
/// </summary>
public IWindow? MainWindow => _windows.Count > 0 ? _windows[0] : null;
}

View File

@@ -20,7 +20,9 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
[nameof(IBorderView.Content)] = MapContent,
[nameof(IBorderStroke.Stroke)] = MapStroke,
[nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness,
["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
[nameof(IPadding.Padding)] = MapPadding,
};
@@ -55,13 +57,25 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
public static void MapContent(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (handler.PlatformView is null || handler.MauiContext is null) return;
handler.PlatformView.ClearChildren();
if (border.PresentedContent?.Handler?.PlatformView is SkiaView skiaContent)
var content = border.PresentedContent;
if (content != null)
{
handler.PlatformView.AddChild(skiaContent);
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
Console.WriteLine($"[BorderHandler] Adding content: {skiaContent.GetType().Name}");
handler.PlatformView.AddChild(skiaContent);
}
}
}
@@ -91,6 +105,17 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
}
}
public static void MapBackgroundColor(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapPadding(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
@@ -101,4 +126,33 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
handler.PlatformView.PaddingRight = (float)padding.Right;
handler.PlatformView.PaddingBottom = (float)padding.Bottom;
}
public static void MapStrokeShape(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
// StrokeShape is on the Border control class, not IBorderView interface
if (border is not Border borderControl) return;
var shape = borderControl.StrokeShape;
if (shape is Microsoft.Maui.Controls.Shapes.RoundRectangle roundRect)
{
// RoundRectangle can have different corner radii, but we use a uniform one
// Take the top-left corner as the uniform radius
var cornerRadius = roundRect.CornerRadius;
handler.PlatformView.CornerRadius = (float)cornerRadius.TopLeft;
}
else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle)
{
handler.PlatformView.CornerRadius = 0;
}
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
{
// For ellipse, use half the min dimension as corner radius
// This will be applied during rendering when bounds are known
handler.PlatformView.CornerRadius = float.MaxValue; // Marker for "fully rounded"
}
handler.PlatformView.Invalidate();
}
}

View File

@@ -0,0 +1,67 @@
// 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.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for BoxView on Linux.
/// </summary>
public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
{
public static IPropertyMapper<BoxView, BoxViewHandler> Mapper =
new PropertyMapper<BoxView, BoxViewHandler>(ViewMapper)
{
[nameof(BoxView.Color)] = MapColor,
[nameof(BoxView.CornerRadius)] = MapCornerRadius,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public BoxViewHandler() : base(Mapper)
{
}
protected override SkiaBoxView CreatePlatformView()
{
return new SkiaBoxView();
}
public static void MapColor(BoxViewHandler handler, BoxView boxView)
{
if (boxView.Color != null)
{
handler.PlatformView.Color = new SKColor(
(byte)(boxView.Color.Red * 255),
(byte)(boxView.Color.Green * 255),
(byte)(boxView.Color.Blue * 255),
(byte)(boxView.Color.Alpha * 255));
}
}
public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView)
{
handler.PlatformView.CornerRadius = (float)boxView.CornerRadius.TopLeft;
}
public static void MapBackground(BoxViewHandler handler, BoxView boxView)
{
if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView)
{
if (boxView.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = boxView.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -60,6 +60,21 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
platformView.Clicked += OnClicked;
platformView.Pressed += OnPressed;
platformView.Released += OnReleased;
// Manually map all properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView != null)
{
MapText(this, VirtualView);
MapTextColor(this, VirtualView);
MapBackground(this, VirtualView);
MapFont(this, VirtualView);
MapPadding(this, VirtualView);
MapCornerRadius(this, VirtualView);
MapBorderColor(this, VirtualView);
MapBorderWidth(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaButton platformView)
@@ -105,7 +120,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
var background = button.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
// Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = solidBrush.Color.ToSKColor();
}
handler.PlatformView.Invalidate();
}
@@ -156,6 +172,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
public static void MapIsEnabled(ButtonHandler handler, IButton button)
{
Console.WriteLine($"[ButtonHandler] MapIsEnabled called - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}

View File

@@ -20,6 +20,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
[nameof(IView.Background)] = MapBackground,
[nameof(IPadding.Padding)] = MapPadding,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -47,6 +48,18 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
platformView.Clicked += OnClicked;
platformView.Pressed += OnPressed;
platformView.Released += OnReleased;
// Manually map all properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView != null)
{
MapStrokeColor(this, VirtualView);
MapStrokeThickness(this, VirtualView);
MapCornerRadius(this, VirtualView);
MapBackground(this, VirtualView);
MapPadding(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaButton platformView)
@@ -88,7 +101,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
// Set ButtonBackgroundColor (used for rendering) not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor();
}
}
@@ -103,6 +117,14 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
(float)padding.Right,
(float)padding.Bottom);
}
public static void MapIsEnabled(ButtonHandler handler, IButton button)
{
if (handler.PlatformView is null) return;
Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}
}
/// <summary>
@@ -124,6 +146,21 @@ public partial class TextButtonHandler : ButtonHandler
{
}
protected override void ConnectHandler(SkiaButton platformView)
{
base.ConnectHandler(platformView);
// Manually map text properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView is ITextButton textButton)
{
MapText(this, textButton);
MapTextColor(this, textButton);
MapFont(this, textButton);
MapCharacterSpacing(this, textButton);
}
}
public static void MapText(TextButtonHandler handler, ITextButton button)
{
if (handler.PlatformView is null) return;

View File

@@ -19,6 +19,8 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
@@ -90,4 +92,22 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox)
{
if (checkBox.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(CheckBoxHandler handler, ICheckBox checkBox)
{
if (checkBox is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -18,6 +18,8 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.Background)] = MapBackground,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
};
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -83,4 +85,32 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.VerticalOptions = checkBox.VerticalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Fill
};
}
public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalOptions = checkBox.HorizontalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}

View File

@@ -15,6 +15,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary>
public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCollectionView>
{
private bool _isUpdatingSelection;
public static IPropertyMapper<CollectionView, CollectionViewHandler> Mapper =
new PropertyMapper<CollectionView, CollectionViewHandler>(ViewHandler.ViewMapper)
{
@@ -36,6 +38,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout,
[nameof(IView.Background)] = MapBackground,
[nameof(CollectionView.BackgroundColor)] = MapBackgroundColor,
};
public static CommandMapper<CollectionView, CollectionViewHandler> CommandMapper =
@@ -76,21 +79,34 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e)
{
if (VirtualView is null) return;
if (VirtualView is null || _isUpdatingSelection) return;
// Update virtual view selection
if (VirtualView.SelectionMode == SelectionMode.Single)
try
{
VirtualView.SelectedItem = e.CurrentSelection.FirstOrDefault();
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
// Clear and update selected items
VirtualView.SelectedItems.Clear();
foreach (var item in e.CurrentSelection)
_isUpdatingSelection = true;
// Update virtual view selection
if (VirtualView.SelectionMode == SelectionMode.Single)
{
VirtualView.SelectedItems.Add(item);
var newItem = e.CurrentSelection.FirstOrDefault();
if (!Equals(VirtualView.SelectedItem, newItem))
{
VirtualView.SelectedItem = newItem;
}
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
// Clear and update selected items
VirtualView.SelectedItems.Clear();
foreach (var item in e.CurrentSelection)
{
VirtualView.SelectedItems.Add(item);
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
@@ -118,7 +134,65 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView)
{
handler.PlatformView?.Invalidate();
if (handler.PlatformView is null || handler.MauiContext is null) return;
var template = collectionView.ItemTemplate;
if (template != null)
{
// Set up a renderer that creates views from the DataTemplate
handler.PlatformView.ItemViewCreator = (item) =>
{
try
{
// Create view from template
var content = template.CreateContent();
if (content is View view)
{
// Set binding context FIRST so bindings evaluate
view.BindingContext = item;
// Force binding evaluation by accessing the visual tree
// This ensures child bindings are evaluated before handler creation
PropagateBindingContext(view, item);
// Create handler for the view
if (view.Handler == null && handler.MauiContext != null)
{
view.Handler = view.ToHandler(handler.MauiContext);
}
if (view.Handler?.PlatformView is SkiaView skiaView)
{
return skiaView;
}
}
else if (content is ViewCell cell)
{
cell.BindingContext = item;
var cellView = cell.View;
if (cellView != null)
{
if (cellView.Handler == null && handler.MauiContext != null)
{
cellView.Handler = cellView.ToHandler(handler.MauiContext);
}
if (cellView.Handler?.PlatformView is SkiaView skiaView)
{
return skiaView;
}
}
}
}
catch
{
// Ignore template creation errors
}
return null;
};
}
handler.PlatformView.Invalidate();
}
public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView)
@@ -146,19 +220,40 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
try
{
handler._isUpdatingSelection = true;
if (!Equals(handler.PlatformView.SelectedItem, collectionView.SelectedItem))
{
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
}
}
finally
{
handler._isUpdatingSelection = false;
}
}
public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
// Sync selected items
var selectedItems = collectionView.SelectedItems;
if (selectedItems != null && selectedItems.Count > 0)
try
{
handler.PlatformView.SelectedItem = selectedItems.First();
handler._isUpdatingSelection = true;
// Sync selected items
var selectedItems = collectionView.SelectedItems;
if (selectedItems != null && selectedItems.Count > 0)
{
handler.PlatformView.SelectedItem = selectedItems.First();
}
}
finally
{
handler._isUpdatingSelection = false;
}
}
@@ -214,12 +309,26 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
{
if (handler.PlatformView is null) return;
// Don't override if BackgroundColor is explicitly set
if (collectionView.BackgroundColor is not null)
return;
if (collectionView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
}
}
public static void MapBackgroundColor(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
if (collectionView.BackgroundColor is not null)
{
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor.ToSKColor();
}
}
public static void MapScrollTo(CollectionViewHandler handler, CollectionView collectionView, object? args)
{
if (handler.PlatformView is null || args is not ScrollToRequestEventArgs scrollArgs)
@@ -234,4 +343,32 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated);
}
}
/// <summary>
/// Recursively propagates binding context to all child views to force binding evaluation.
/// </summary>
private static void PropagateBindingContext(View view, object? bindingContext)
{
view.BindingContext = bindingContext;
// Propagate to children
if (view is Layout layout)
{
foreach (var child in layout.Children)
{
if (child is View childView)
{
PropagateBindingContext(childView, bindingContext);
}
}
}
else if (view is ContentView contentView && contentView.Content != null)
{
PropagateBindingContext(contentView.Content, bindingContext);
}
else if (view is Border border && border.Content is View borderContent)
{
PropagateBindingContext(borderContent, bindingContext);
}
}
}

View File

@@ -31,6 +31,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
[nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<IEditor, EditorHandler> CommandMapper =
@@ -82,6 +83,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
{
if (handler.PlatformView is null) return;
handler.PlatformView.Text = editor.Text ?? "";
handler.PlatformView.Invalidate();
}
public static void MapPlaceholder(EditorHandler handler, IEditor editor)
@@ -165,4 +167,15 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapBackgroundColor(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView is null) return;
if (editor is VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -30,6 +30,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
[nameof(IEntry.ReturnType)] = MapReturnType,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IEntry.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
@@ -186,4 +187,13 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
}
handler.PlatformView.Invalidate();
}
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
{
if (entry is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -85,7 +85,10 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
if (handler.PlatformView is null) return;
if (handler.PlatformView.Text != entry.Text)
{
handler.PlatformView.Text = entry.Text ?? string.Empty;
handler.PlatformView.Invalidate();
}
}
public static void MapTextColor(EntryHandler handler, IEntry entry)

104
Handlers/FrameHandler.cs Normal file
View File

@@ -0,0 +1,104 @@
// 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.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for Frame on Linux using SkiaFrame.
/// </summary>
public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
{
public static IPropertyMapper<Frame, FrameHandler> Mapper =
new PropertyMapper<Frame, FrameHandler>(ViewMapper)
{
[nameof(Frame.BorderColor)] = MapBorderColor,
[nameof(Frame.CornerRadius)] = MapCornerRadius,
[nameof(Frame.HasShadow)] = MapHasShadow,
[nameof(Frame.BackgroundColor)] = MapBackgroundColor,
[nameof(Frame.Padding)] = MapPadding,
[nameof(Frame.Content)] = MapContent,
};
public FrameHandler() : base(Mapper)
{
}
public FrameHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper)
{
}
protected override SkiaFrame CreatePlatformView()
{
return new SkiaFrame();
}
public static void MapBorderColor(FrameHandler handler, Frame frame)
{
if (frame.BorderColor != null)
{
handler.PlatformView.Stroke = new SKColor(
(byte)(frame.BorderColor.Red * 255),
(byte)(frame.BorderColor.Green * 255),
(byte)(frame.BorderColor.Blue * 255),
(byte)(frame.BorderColor.Alpha * 255));
}
}
public static void MapCornerRadius(FrameHandler handler, Frame frame)
{
handler.PlatformView.CornerRadius = frame.CornerRadius;
}
public static void MapHasShadow(FrameHandler handler, Frame frame)
{
handler.PlatformView.HasShadow = frame.HasShadow;
}
public static void MapBackgroundColor(FrameHandler handler, Frame frame)
{
if (frame.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = new SKColor(
(byte)(frame.BackgroundColor.Red * 255),
(byte)(frame.BackgroundColor.Green * 255),
(byte)(frame.BackgroundColor.Blue * 255),
(byte)(frame.BackgroundColor.Alpha * 255));
}
}
public static void MapPadding(FrameHandler handler, Frame frame)
{
handler.PlatformView.SetPadding(
(float)frame.Padding.Left,
(float)frame.Padding.Top,
(float)frame.Padding.Right,
(float)frame.Padding.Bottom);
}
public static void MapContent(FrameHandler handler, Frame frame)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
handler.PlatformView.ClearChildren();
var content = frame.Content;
if (content != null)
{
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
handler.PlatformView.AddChild(skiaContent);
}
}
}
}

View File

@@ -26,6 +26,8 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
[nameof(ILabel.Padding)] = MapPadding,
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
[nameof(ILabel.LineHeight)] = MapLineHeight,
[nameof(ILabel.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
@@ -151,4 +153,22 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
handler.PlatformView.LineHeight = (float)label.LineHeight;
handler.PlatformView.Invalidate();
}
public static void MapBackground(LabelHandler handler, ILabel label)
{
if (label.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(LabelHandler handler, ILabel label)
{
if (label is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -23,8 +23,12 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
[nameof(ILabel.LineHeight)] = MapLineHeight,
["LineBreakMode"] = MapLineBreakMode,
["MaxLines"] = MapMaxLines,
[nameof(IPadding.Padding)] = MapPadding,
[nameof(IView.Background)] = MapBackground,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
};
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -121,6 +125,37 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
handler.PlatformView.LineHeight = (float)label.LineHeight;
}
public static void MapLineBreakMode(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
// LineBreakMode is on Label control, not ILabel interface
if (label is Microsoft.Maui.Controls.Label mauiLabel)
{
handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode switch
{
Microsoft.Maui.LineBreakMode.NoWrap => Platform.LineBreakMode.NoWrap,
Microsoft.Maui.LineBreakMode.WordWrap => Platform.LineBreakMode.WordWrap,
Microsoft.Maui.LineBreakMode.CharacterWrap => Platform.LineBreakMode.CharacterWrap,
Microsoft.Maui.LineBreakMode.HeadTruncation => Platform.LineBreakMode.HeadTruncation,
Microsoft.Maui.LineBreakMode.TailTruncation => Platform.LineBreakMode.TailTruncation,
Microsoft.Maui.LineBreakMode.MiddleTruncation => Platform.LineBreakMode.MiddleTruncation,
_ => Platform.LineBreakMode.TailTruncation
};
}
}
public static void MapMaxLines(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
// MaxLines is on Label control, not ILabel interface
if (label is Microsoft.Maui.Controls.Label mauiLabel)
{
handler.PlatformView.MaxLines = mauiLabel.MaxLines;
}
}
public static void MapPadding(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
@@ -142,4 +177,32 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
handler.PlatformView.VerticalOptions = label.VerticalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalOptions = label.HorizontalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}

View File

@@ -17,7 +17,9 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
public static IPropertyMapper<ILayout, LayoutHandler> Mapper = new PropertyMapper<ILayout, LayoutHandler>(ViewHandler.ViewMapper)
{
[nameof(ILayout.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
[nameof(IPadding.Padding)] = MapPadding,
};
/// <summary>
@@ -53,8 +55,46 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
base.ConnectHandler(platformView);
// Explicitly map BackgroundColor since it may be set before handler creation
// (e.g., in ItemTemplates for CollectionView)
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
platformView.Invalidate();
}
// Add existing children (important for template-created views)
if (VirtualView is ILayout layout && MauiContext != null)
{
for (int i = 0; i < layout.Count; i++)
{
var child = layout[i];
if (child == null) continue;
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
}
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
platformView.AddChild(skiaChild);
}
}
}
}
public static void MapBackground(LayoutHandler handler, ILayout layout)
{
// Don't override if BackgroundColor is explicitly set
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
return;
var background = layout.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
@@ -63,12 +103,36 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
handler.PlatformView.Invalidate();
}
public static void MapBackgroundColor(LayoutHandler handler, ILayout layout)
{
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
{
handler.PlatformView.ClipToBounds = layout.ClipsToBounds;
handler.PlatformView.Invalidate();
}
public static void MapPadding(LayoutHandler handler, ILayout layout)
{
if (layout is IPadding paddable)
{
var padding = paddable.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.InvalidateMeasure();
handler.PlatformView.Invalidate();
}
}
public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg)
{
if (arg is LayoutHandlerUpdate update)
@@ -194,9 +258,16 @@ public partial class GridHandler : LayoutHandler
{
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
};
public GridHandler() : base(Mapper)
public static new CommandMapper<IGridLayout, GridHandler> GridCommandMapper = new(LayoutHandler.CommandMapper)
{
["Add"] = MapGridAdd,
};
public GridHandler() : base(Mapper, GridCommandMapper)
{
}
@@ -205,6 +276,52 @@ public partial class GridHandler : LayoutHandler
return new SkiaGrid();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
Console.WriteLine($"[GridHandler.ConnectHandler] Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
base.ConnectHandler(platformView);
// Map definitions on connect
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
{
Console.WriteLine($"[GridHandler.ConnectHandler] Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
UpdateRowDefinitions(grid, gridLayout);
UpdateColumnDefinitions(grid, gridLayout);
// Add existing children (important for template-created views)
for (int i = 0; i < gridLayout.Count; i++)
{
var child = gridLayout[i];
if (child == null) continue;
Console.WriteLine($"[GridHandler.ConnectHandler] Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
}
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (child is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
Console.WriteLine($"[GridHandler.ConnectHandler] Adding child[{i}] at row={row}, col={column}");
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
}
}
Console.WriteLine($"[GridHandler.ConnectHandler] Grid now has {grid.Children.Count} SkiaView children");
}
}
public static void MapColumnSpacing(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
@@ -222,6 +339,79 @@ public partial class GridHandler : LayoutHandler
grid.Invalidate();
}
}
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
{
UpdateRowDefinitions(grid, layout);
grid.InvalidateMeasure();
grid.Invalidate();
}
}
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
{
UpdateColumnDefinitions(grid, layout);
grid.InvalidateMeasure();
grid.Invalidate();
}
}
private static void UpdateRowDefinitions(SkiaGrid grid, IGridLayout layout)
{
grid.RowDefinitions.Clear();
foreach (var rowDef in layout.RowDefinitions)
{
var height = rowDef.Height;
if (height.IsAbsolute)
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Absolute));
else if (height.IsAuto)
grid.RowDefinitions.Add(GridLength.Auto);
else // Star
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Star));
}
}
private static void UpdateColumnDefinitions(SkiaGrid grid, IGridLayout layout)
{
grid.ColumnDefinitions.Clear();
foreach (var colDef in layout.ColumnDefinitions)
{
var width = colDef.Width;
if (width.IsAbsolute)
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Absolute));
else if (width.IsAuto)
grid.ColumnDefinitions.Add(GridLength.Auto);
else // Star
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Star));
}
}
public static void MapGridAdd(GridHandler handler, ILayout layout, object? arg)
{
if (arg is LayoutHandlerUpdate update && handler.PlatformView is SkiaGrid grid)
{
var childHandler = update.View.Handler;
if (childHandler?.PlatformView is SkiaView skiaView)
{
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (update.View is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
grid.AddChild(skiaView, row, column, rowSpan, columnSpan);
}
}
}
}
/// <summary>

View File

@@ -17,6 +17,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
{
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
[nameof(IView.Background)] = MapBackground,
[nameof(IPadding.Padding)] = MapPadding,
};
public static CommandMapper<ILayout, LayoutHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -42,6 +43,38 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
base.ConnectHandler(platformView);
// Create handlers for all children and add them to the platform view
if (VirtualView == null || MauiContext == null) return;
// Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
}
for (int i = 0; i < VirtualView.Count; i++)
{
var child = VirtualView[i];
if (child == null) continue;
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
}
// Add child's platform view to our layout
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
platformView.AddChild(skiaChild);
}
}
}
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
{
if (handler.PlatformView == null) return;
@@ -102,6 +135,23 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Force re-layout
handler.PlatformView?.InvalidateMeasure();
}
public static void MapPadding(LayoutHandler handler, ILayout layout)
{
if (handler.PlatformView == null) return;
if (layout is IPadding paddable)
{
var padding = paddable.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.InvalidateMeasure();
handler.PlatformView.Invalidate();
}
}
}
/// <summary>
@@ -138,6 +188,29 @@ public partial class StackLayoutHandler : LayoutHandler
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
// Set orientation first
if (platformView is SkiaStackLayout stackLayout && VirtualView is IStackLayout stackView)
{
// Determine orientation based on view type
if (VirtualView is Microsoft.Maui.Controls.HorizontalStackLayout)
{
stackLayout.Orientation = StackOrientation.Horizontal;
}
else if (VirtualView is Microsoft.Maui.Controls.VerticalStackLayout ||
VirtualView is Microsoft.Maui.Controls.StackLayout)
{
stackLayout.Orientation = StackOrientation.Vertical;
}
stackLayout.Spacing = (float)stackView.Spacing;
}
// Let base handle children
base.ConnectHandler(platformView);
}
public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout)
{
if (handler.PlatformView is SkiaStackLayout stackLayout)
@@ -156,6 +229,8 @@ public partial class GridHandler : LayoutHandler
{
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
};
public GridHandler() : base(Mapper)
@@ -167,6 +242,80 @@ public partial class GridHandler : LayoutHandler
return new SkiaGrid();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
try
{
// Don't call base - we handle children specially for Grid
if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return;
Console.WriteLine($"[GridHandler] ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols");
// Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
}
// Explicitly map Padding since it may be set before handler creation
if (VirtualView is IPadding paddable)
{
var padding = paddable.Padding;
platformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
}
// Map row/column definitions first
MapRowDefinitions(this, gridLayout);
MapColumnDefinitions(this, gridLayout);
// Add each child with its row/column position
for (int i = 0; i < gridLayout.Count; i++)
{
var child = gridLayout[i];
if (child == null) continue;
Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}");
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
}
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (child is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
// Add child's platform view to our grid
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
Console.WriteLine($"[GridHandler] Added child {i} to grid");
}
}
Console.WriteLine($"[GridHandler] ConnectHandler complete");
}
catch (Exception ex)
{
Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}");
throw;
}
}
public static void MapRowSpacing(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
@@ -182,4 +331,38 @@ public partial class GridHandler : LayoutHandler
grid.ColumnSpacing = (float)layout.ColumnSpacing;
}
}
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is not SkiaGrid grid) return;
grid.RowDefinitions.Clear();
foreach (var rowDef in layout.RowDefinitions)
{
var height = rowDef.Height;
if (height.IsAbsolute)
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
else if (height.IsAuto)
grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
else // Star
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Star));
}
}
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is not SkiaGrid grid) return;
grid.ColumnDefinitions.Clear();
foreach (var colDef in layout.ColumnDefinitions)
{
var width = colDef.Width;
if (width.IsAbsolute)
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
else if (width.IsAuto)
grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
else // Star
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Star));
}
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using SkiaSharp;
using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -50,10 +51,15 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
platformView.Popped += OnPopped;
platformView.PoppedToRoot += OnPoppedToRoot;
// Set initial root page if exists
if (VirtualView.CurrentPage != null)
// Subscribe to navigation events from virtual view
if (VirtualView != null)
{
SetupInitialPage();
VirtualView.Pushed += OnVirtualViewPushed;
VirtualView.Popped += OnVirtualViewPopped;
VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot;
// Set up initial navigation stack
SetupNavigationStack();
}
}
@@ -62,16 +68,195 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
platformView.Pushed -= OnPushed;
platformView.Popped -= OnPopped;
platformView.PoppedToRoot -= OnPoppedToRoot;
if (VirtualView != null)
{
VirtualView.Pushed -= OnVirtualViewPushed;
VirtualView.Popped -= OnVirtualViewPopped;
VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot;
}
base.DisconnectHandler(platformView);
}
private void SetupInitialPage()
private void SetupNavigationStack()
{
var currentPage = VirtualView.CurrentPage;
if (currentPage?.Handler?.PlatformView is SkiaPage skiaPage)
if (VirtualView == null || PlatformView == null || MauiContext == null) return;
// Get all pages in the navigation stack
var pages = VirtualView.Navigation.NavigationStack.ToList();
Console.WriteLine($"[NavigationPageHandler] Setting up {pages.Count} pages");
// If no pages in stack, check CurrentPage
if (pages.Count == 0 && VirtualView.CurrentPage != null)
{
PlatformView.SetRootPage(skiaPage);
Console.WriteLine($"[NavigationPageHandler] No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
pages.Add(VirtualView.CurrentPage);
}
foreach (var page in pages)
{
// Ensure the page has a handler
if (page.Handler == null)
{
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
page.Handler = page.ToHandler(MauiContext);
}
Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
Console.WriteLine($"[NavigationPageHandler] Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
if (page.Handler?.PlatformView is SkiaPage skiaPage)
{
// Set navigation bar properties
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = page.Title ?? "";
Console.WriteLine($"[NavigationPageHandler] SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
// If content is null, try to get it from ContentPage
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
{
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
skiaPage.Content = skiaContent;
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
}
}
// Map toolbar items
MapToolbarItems(skiaPage, page);
if (PlatformView.StackDepth == 0)
{
Console.WriteLine($"[NavigationPageHandler] Setting root page: {page.Title}");
PlatformView.SetRootPage(skiaPage);
}
else
{
Console.WriteLine($"[NavigationPageHandler] Pushing page: {page.Title}");
PlatformView.Push(skiaPage, false);
}
}
else
{
Console.WriteLine($"[NavigationPageHandler] Failed to get SkiaPage for: {page.Title}");
}
}
}
private readonly Dictionary<Page, (SkiaPage, INotifyCollectionChanged)> _toolbarSubscriptions = new();
private void MapToolbarItems(SkiaPage skiaPage, Page page)
{
if (skiaPage is SkiaContentPage contentPage)
{
Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
contentPage.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems)
{
Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', Order={item.Order}");
// Default and Primary should both be treated as Primary (shown in toolbar)
// Only Secondary goes to overflow menu
var order = item.Order == ToolbarItemOrder.Secondary
? SkiaToolbarItemOrder.Secondary
: SkiaToolbarItemOrder.Primary;
// Create a command that invokes the Clicked event
var toolbarItem = item; // Capture for closure
var clickCommand = new RelayCommand(() =>
{
Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
// Use IMenuItemController to send the click
if (toolbarItem is IMenuItemController menuController)
{
menuController.Activate();
}
else
{
// Fallback: invoke Command if set
toolbarItem.Command?.Execute(toolbarItem.CommandParameter);
}
});
contentPage.ToolbarItems.Add(new SkiaToolbarItem
{
Text = item.Text ?? "",
Order = order,
Command = clickCommand
});
}
// Subscribe to ToolbarItems changes if not already subscribed
if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page))
{
Console.WriteLine($"[NavigationPageHandler] Subscribing to ToolbarItems changes for '{page.Title}'");
notifyCollection.CollectionChanged += (s, e) =>
{
Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}");
MapToolbarItems(skiaPage, page);
skiaPage.Invalidate();
};
_toolbarSubscriptions[page] = (skiaPage, notifyCollection);
}
}
}
private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
try
{
Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}");
if (e.Page == null || PlatformView == null || MauiContext == null) return;
// Ensure the page has a handler
if (e.Page.Handler == null)
{
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
e.Page.Handler = e.Page.ToHandler(MauiContext);
Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
}
if (e.Page.Handler?.PlatformView is SkiaPage skiaPage)
{
Console.WriteLine($"[NavigationPageHandler] Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor;
Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items");
MapToolbarItems(skiaPage, e.Page);
Console.WriteLine($"[NavigationPageHandler] Pushing page to platform");
PlatformView.Push(skiaPage, true);
Console.WriteLine($"[NavigationPageHandler] Push complete");
}
}
catch (Exception ex)
{
Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}");
throw;
}
}
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}");
// Pop on the platform side to sync with MAUI navigation
PlatformView?.Pop(true);
}
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot");
PlatformView?.PopToRoot(true);
}
private void OnPushed(object? sender, NavigationEventArgs e)
@@ -81,7 +266,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
private void OnPopped(object? sender, NavigationEventArgs e)
{
// Sync back to virtual view if needed
// Sync back to virtual view - pop from MAUI navigation stack
if (VirtualView?.Navigation.NavigationStack.Count > 1)
{
// Don't trigger another pop on platform side
VirtualView.Navigation.RemovePage(VirtualView.Navigation.NavigationStack.Last());
}
}
private void OnPoppedToRoot(object? sender, NavigationEventArgs e)
@@ -131,14 +321,29 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
public static void MapRequestNavigation(NavigationPageHandler handler, NavigationPage navigationPage, object? args)
{
if (handler.PlatformView is null || args is not NavigationRequest request)
if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request)
return;
Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {request.NavigationStack.Count} pages");
// Handle navigation request
foreach (var page in request.NavigationStack)
foreach (var view in request.NavigationStack)
{
if (view is not Page page) continue;
// Ensure handler exists
if (page.Handler == null)
{
page.Handler = page.ToHandler(handler.MauiContext);
}
if (page.Handler?.PlatformView is SkiaPage skiaPage)
{
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = handler.PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = handler.PlatformView.BarTextColor;
handler.MapToolbarItems(skiaPage, page);
if (handler.PlatformView.StackDepth == 0)
{
handler.PlatformView.SetRootPage(skiaPage);
@@ -151,3 +356,26 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
}
}
}
/// <summary>
/// Simple relay command for invoking actions.
/// </summary>
internal class RelayCommand : System.Windows.Input.ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

View File

@@ -58,6 +58,7 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
private void OnAppearing(object? sender, EventArgs e)
{
Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}");
(VirtualView as IPageController)?.SendAppearing();
}
@@ -133,18 +134,29 @@ public partial class ContentPageHandler : PageHandler
public static void MapContent(ContentPageHandler handler, ContentPage page)
{
if (handler.PlatformView is null) return;
if (handler.PlatformView is null || handler.MauiContext is null) return;
// Get the platform view for the content
var content = page.Content;
if (content != null)
{
// The content's handler should provide the platform view
var contentHandler = content.Handler;
if (contentHandler?.PlatformView is SkiaView skiaContent)
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToHandler(handler.MauiContext);
}
// The content's handler should provide the platform view
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent;
}
else
{
Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
}
}
else
{

View File

@@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using SkiaSharp;
using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -25,6 +26,7 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground,
[nameof(Picker.ItemsSource)] = MapItemsSource,
};
public static CommandMapper<IPicker, PickerHandler> CommandMapper =
@@ -32,6 +34,8 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
{
};
private INotifyCollectionChanged? _itemsCollection;
public PickerHandler() : base(Mapper, CommandMapper)
{
}
@@ -51,6 +55,13 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
base.ConnectHandler(platformView);
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
// Subscribe to items collection changes
if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged items)
{
_itemsCollection = items;
_itemsCollection.CollectionChanged += OnItemsCollectionChanged;
}
// Load items
ReloadItems();
}
@@ -58,9 +69,21 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
protected override void DisconnectHandler(SkiaPicker platformView)
{
platformView.SelectedIndexChanged -= OnSelectedIndexChanged;
if (_itemsCollection != null)
{
_itemsCollection.CollectionChanged -= OnItemsCollectionChanged;
_itemsCollection = null;
}
base.DisconnectHandler(platformView);
}
private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ReloadItems();
}
private void OnSelectedIndexChanged(object? sender, EventArgs e)
{
if (VirtualView is null || PlatformView is null) return;
@@ -130,4 +153,9 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapItemsSource(PickerHandler handler, IPicker picker)
{
handler.ReloadItems();
}
}

View File

@@ -15,6 +15,8 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
[nameof(IProgress.Progress)] = MapProgress,
[nameof(IProgress.ProgressColor)] = MapProgressColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
@@ -40,4 +42,22 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
handler.PlatformView.IsEnabled = progress.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
{
if (progress.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress)
{
if (progress is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -0,0 +1,109 @@
// 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.Handlers;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for ScrollView on Linux using SkiaScrollView.
/// </summary>
public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView>
{
public static IPropertyMapper<IScrollView, ScrollViewHandler> Mapper =
new PropertyMapper<IScrollView, ScrollViewHandler>(ViewMapper)
{
[nameof(IScrollView.Content)] = MapContent,
[nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility,
[nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility,
[nameof(IScrollView.Orientation)] = MapOrientation,
};
public static CommandMapper<IScrollView, ScrollViewHandler> CommandMapper =
new(ViewCommandMapper)
{
[nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo
};
public ScrollViewHandler() : base(Mapper, CommandMapper)
{
}
public ScrollViewHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
protected override SkiaScrollView CreatePlatformView()
{
return new SkiaScrollView();
}
public static void MapContent(ScrollViewHandler handler, IScrollView scrollView)
{
if (handler.PlatformView == null || handler.MauiContext == null)
return;
var content = scrollView.PresentedContent;
if (content != null)
{
Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}");
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent;
}
}
else
{
handler.PlatformView.Content = null;
}
}
public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch
{
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch
{
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.Orientation = scrollView.Orientation switch
{
Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal,
Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both,
Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither,
_ => ScrollOrientation.Vertical
};
}
public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args)
{
if (args is ScrollToRequest request)
{
// Instant means no animation, so we pass !Instant for animated parameter
handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant);
}
}
}

View File

@@ -1,6 +1,7 @@
// 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.Handlers;
using Microsoft.Maui.Graphics;
using SkiaSharp;
@@ -10,13 +11,13 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for Shell on Linux using Skia rendering.
/// </summary>
public partial class ShellHandler : ViewHandler<IView, SkiaShell>
public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{
public static IPropertyMapper<IView, ShellHandler> Mapper = new PropertyMapper<IView, ShellHandler>(ViewHandler.ViewMapper)
public static IPropertyMapper<Shell, ShellHandler> Mapper = new PropertyMapper<Shell, ShellHandler>(ViewHandler.ViewMapper)
{
};
public static CommandMapper<IView, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
public static CommandMapper<Shell, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
@@ -39,12 +40,26 @@ public partial class ShellHandler : ViewHandler<IView, SkiaShell>
base.ConnectHandler(platformView);
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
platformView.Navigated += OnNavigated;
// Subscribe to Shell navigation events
if (VirtualView != null)
{
VirtualView.Navigating += OnShellNavigating;
VirtualView.Navigated += OnShellNavigated;
}
}
protected override void DisconnectHandler(SkiaShell platformView)
{
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
platformView.Navigated -= OnNavigated;
if (VirtualView != null)
{
VirtualView.Navigating -= OnShellNavigating;
VirtualView.Navigated -= OnShellNavigated;
}
base.DisconnectHandler(platformView);
}
@@ -55,6 +70,24 @@ public partial class ShellHandler : ViewHandler<IView, SkiaShell>
private void OnNavigated(object? sender, ShellNavigationEventArgs e)
{
// Handle navigation events
// Handle platform navigation events
}
private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e)
{
Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}");
// Route to platform view
if (PlatformView != null && e.Target?.Location != null)
{
var route = e.Target.Location.ToString().TrimStart('/');
Console.WriteLine($"[ShellHandler] Routing to: {route}");
PlatformView.GoToAsync(route);
}
}
private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{
Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}");
}
}

View File

@@ -19,6 +19,8 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
[nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor,
[nameof(ISlider.ThumbColor)] = MapThumbColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
@@ -100,4 +102,22 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
handler.PlatformView.IsEnabled = slider.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(SliderHandler handler, ISlider slider)
{
if (slider.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(SliderHandler handler, ISlider slider)
{
if (slider is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -22,6 +22,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
[nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor,
[nameof(ISlider.ThumbColor)] = MapThumbColor,
[nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -48,6 +49,15 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
platformView.ValueChanged += OnValueChanged;
platformView.DragStarted += OnDragStarted;
platformView.DragCompleted += OnDragCompleted;
// Sync properties that may have been set before handler connection
if (VirtualView != null)
{
MapMinimum(this, VirtualView);
MapMaximum(this, VirtualView);
MapValue(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaSlider platformView)
@@ -133,4 +143,11 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = slider.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -16,6 +16,8 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
[nameof(ISwitch.TrackColor)] = MapTrackColor,
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
@@ -71,4 +73,22 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
handler.PlatformView.IsEnabled = @switch.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(SwitchHandler handler, ISwitch @switch)
{
if (@switch.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -0,0 +1,96 @@
// 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.Handlers;
using Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for WebView control on Linux using WebKitGTK.
/// </summary>
public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
{
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IWebView.Source)] = MapSource,
};
public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
[nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload,
};
public WebViewHandler() : base(Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaWebView CreatePlatformView()
{
return new SkiaWebView();
}
protected override void ConnectHandler(SkiaWebView platformView)
{
base.ConnectHandler(platformView);
platformView.Navigating += OnNavigating;
platformView.Navigated += OnNavigated;
}
protected override void DisconnectHandler(SkiaWebView platformView)
{
platformView.Navigating -= OnNavigating;
platformView.Navigated -= OnNavigated;
base.DisconnectHandler(platformView);
}
private void OnNavigating(object? sender, WebNavigatingEventArgs e)
{
// Forward to virtual view if needed
}
private void OnNavigated(object? sender, WebNavigatedEventArgs e)
{
// Forward to virtual view if needed
}
public static void MapSource(WebViewHandler handler, IWebView webView)
{
if (handler.PlatformView == null) return;
var source = webView.Source;
if (source is UrlWebViewSource urlSource)
{
handler.PlatformView.Source = urlSource.Url ?? "";
}
else if (source is HtmlWebViewSource htmlSource)
{
handler.PlatformView.Html = htmlSource.Html ?? "";
}
}
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.GoBack();
}
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.GoForward();
}
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.Reload();
}
}

View File

@@ -141,6 +141,7 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
/// <summary>
/// Skia window wrapper for Linux display servers.
/// Handles rendering of content and popup overlays automatically.
/// </summary>
public class SkiaWindow
{
@@ -164,6 +165,28 @@ public class SkiaWindow
}
}
/// <summary>
/// Renders the window content and popup overlays to the canvas.
/// This should be called by the platform rendering loop.
/// </summary>
public void Render(SKCanvas canvas)
{
// Clear background
canvas.Clear(SKColors.White);
// Draw main content
if (_content != null)
{
_content.Measure(new SKSize(_width, _height));
_content.Arrange(new SKRect(0, 0, _width, _height));
_content.Draw(canvas);
}
// Draw popup overlays on top (dropdowns, date pickers, etc.)
// This ensures popups always render above all other content
SkiaView.DrawPopupOverlays(canvas);
}
public string Title
{
get => _title;