3 Commits

Author SHA1 Message Date
2a4e35cd39 Fix compilation: restore clean RC1 codebase
- Restore clean BindableProperty.Create syntax from RC1 commit
- Remove decompiler artifacts with mangled delegate types
- Add Svg.Skia package reference for icon support
- Fix duplicate type definitions
- Library now compiles successfully (0 errors)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 07:43:44 -05:00
33914bf572 Add fixfuckup.md - recovery plan for lost XAML and AppThemeBinding work 2026-01-01 06:30:34 -05:00
1f096c38dc Update with recovered code from VM binaries (Jan 1)
Recovered from decompiled OpenMaui.Controls.Linux.dll:
- SkiaShell.cs: FlyoutHeader, FlyoutFooter, scroll support (918 -> 1325 lines)
- X11Window.cs: Cursor support (XCreateFontCursor, XDefineCursor)
- All handlers with dark mode support
- All services with latest implementations
- LinuxApplication with theme change handling
2026-01-01 06:22:48 -05:00
409 changed files with 15382 additions and 42596 deletions

View File

@@ -1,39 +1,46 @@
# OpenMaui Linux CI Pipeline # OpenMaui Linux CI/CD Pipeline for Gitea
name: CI name: CI
on: on:
push: push:
branches: [main, final, develop] branches: [main, develop]
pull_request: pull_request:
branches: [main] branches: [main]
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_ROOT: C:\dotnet
jobs: jobs:
build-linux: build:
name: Build (Linux) name: Build and Test
runs-on: linux-latest runs-on: windows
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: C:\dotnet\dotnet.exe restore
- name: Build - name: Build
run: dotnet build --configuration Release --no-restore run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
- name: Run tests - name: Run tests
run: dotnet test --configuration Release --no-build --verbosity normal run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
continue-on-error: true continue-on-error: true
- name: Pack NuGet - name: Pack NuGet (preview)
run: dotnet pack --configuration Release --no-build -o ./nupkg run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
- name: Upload NuGet packages - name: List NuGet packages
uses: actions/upload-artifact@v3 run: dir .\nupkg\
with:
name: nuget-packages - name: Push to NuGet.org
path: ./nupkg/*.nupkg if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
foreach ($pkg in (Get-ChildItem .\nupkg\*.nupkg)) {
C:\dotnet\dotnet.exe nuget push $pkg.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
}
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

View File

@@ -1,7 +1,9 @@
# OpenMaui Linux Release - Publish to Package Registries # OpenMaui Linux Release - Publish to NuGet
name: Release name: Release to NuGet
on: on:
release:
types: [published]
push: push:
tags: tags:
- 'v*' - 'v*'
@@ -9,59 +11,31 @@ on:
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_ROOT: C:\dotnet
jobs: jobs:
release: release:
name: Build and Publish name: Build and Publish to NuGet
runs-on: linux-latest runs-on: windows
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Get version from tag
id: version
run: |
TAG="${{ gitea.ref_name }}"
VERSION="${TAG#v}"
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: C:\dotnet\dotnet.exe restore
- name: Build - name: Build
run: dotnet build --configuration Release --no-restore run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
- name: Run tests - name: Run tests
run: dotnet test --configuration Release --no-build --verbosity normal run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
continue-on-error: true continue-on-error: true
- name: Pack NuGet package - name: Pack NuGet package
run: dotnet pack --configuration Release --no-build -o ./nupkg -p:PackageVersion=${{ steps.version.outputs.VERSION }} run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
- name: Upload NuGet packages
uses: actions/upload-artifact@v3
with:
name: nuget-packages
path: ./nupkg/*.nupkg
- name: Publish to Gitea Packages
run: |
for pkg in ./nupkg/*.nupkg; do
dotnet nuget push "$pkg" \
--api-key ${{ secrets.GITEATOKEN }} \
--source https://git.marketally.ai/api/packages/${{ gitea.repository_owner }}/nuget/index.json \
--skip-duplicate
done
- name: Publish to NuGet.org - name: Publish to NuGet.org
if: ${{ secrets.NUGET_API_KEY != '' }}
run: | run: |
for pkg in ./nupkg/*.nupkg; do foreach ($pkg in (Get-ChildItem .\nupkg\*.nupkg)) {
dotnet nuget push "$pkg" \ C:\dotnet\dotnet.exe nuget push $pkg.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
--api-key ${{ secrets.NUGET_API_KEY }} \ }
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
done
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

5
.gitignore vendored
View File

@@ -47,8 +47,3 @@ coverage*.xml
# Publish output # Publish output
publish/ publish/
mauiplan.md
# Development artifacts
out.xml
MERGE_TRACKING.md

View File

@@ -1,8 +0,0 @@
{
"id": "note-1770523035702-znjlto6vn",
"title": "check later",
"content": "● Review in OpenMaui:\n\n | Feature Used | OpenMaui Support | Notes |\n |--------------------------------|------------------|-------------------------------|\n | Controls | | |\n | ContentPage | ✓ | SkiaContentPage |\n | Grid | ✓ | SkiaGrid |\n | VerticalStackLayout | ✓ | SkiaStackLayout |\n | HorizontalStackLayout | ✓ | SkiaStackLayout |\n | Label | ✓ | SkiaLabel |\n | Button | ✓ | SkiaButton |\n | CheckBox | ✓ | SkiaCheckBox |\n | ProgressBar | ✓ | SkiaProgressBar |\n | Border | ✓ | SkiaBorder |\n | CollectionView | ✓ | SkiaCollectionView |\n | XAML Features | | |\n | Data Binding | ✓ | Full BindableProperty support |\n | StaticResource | ✓ | ResourceDictionary |\n | DataTemplate | ✓ | |\n | IValueConverter | ✓ | |\n | Potential Gaps | | |\n | StrokeShape=\"RoundRectangle 8\" | ? | May need different syntax |\n | FormattedText / Span | ? | Didn't see explicit support |\n | x:DataType compiled bindings | ? | May work, may not |\n | CollectionView.EmptyView | ? | Not confirmed |\n\n Honest assessment: The core controls are there, but I'd need to check:\n\n 1. Does SkiaBorder support StrokeShape with corner radius?\n 2. Does SkiaLabel support FormattedText with multiple Span elements?\n 3. Does SkiaCollectionView support EmptyView?\n\n Want me to dig into the OpenMaui source to verify these specific features?",
"createdAt": 1770523035699,
"updatedAt": 1771217844920,
"tags": []
}

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,182 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux;
public static class AnimationManager
{
private class RunningAnimation
{
public required SkiaView View { get; set; }
public string PropertyName { get; set; } = "";
public double StartValue { get; set; }
public double EndValue { get; set; }
public DateTime StartTime { get; set; }
public uint Duration { get; set; }
public Easing Easing { get; set; } = Easing.Linear;
public required TaskCompletionSource<bool> Completion { get; set; }
public CancellationToken Token { get; set; }
}
private static readonly List<RunningAnimation> _animations = new();
private static bool _isRunning;
private static CancellationTokenSource? _cts;
private static void EnsureRunning()
{
if (!_isRunning)
{
_isRunning = true;
_cts = new CancellationTokenSource();
_ = RunAnimationLoop(_cts.Token);
}
}
private static async Task RunAnimationLoop(CancellationToken token)
{
while (!token.IsCancellationRequested && _animations.Count > 0)
{
var now = DateTime.UtcNow;
var completed = new List<RunningAnimation>();
foreach (var animation in _animations.ToList())
{
if (animation.Token.IsCancellationRequested)
{
completed.Add(animation);
animation.Completion.TrySetResult(false);
continue;
}
var progress = Math.Clamp(
(now - animation.StartTime).TotalMilliseconds / animation.Duration,
0.0, 1.0);
var easedProgress = animation.Easing.Ease(progress);
var value = animation.StartValue + (animation.EndValue - animation.StartValue) * easedProgress;
SetProperty(animation.View, animation.PropertyName, value);
if (progress >= 1.0)
{
completed.Add(animation);
animation.Completion.TrySetResult(true);
}
}
foreach (var animation in completed)
{
_animations.Remove(animation);
}
if (_animations.Count == 0)
{
_isRunning = false;
return;
}
await Task.Delay(16, token);
}
_isRunning = false;
}
private static void SetProperty(SkiaView view, string propertyName, double value)
{
switch (propertyName)
{
case nameof(SkiaView.Opacity):
view.Opacity = (float)value;
break;
case nameof(SkiaView.Scale):
view.Scale = value;
break;
case nameof(SkiaView.ScaleX):
view.ScaleX = value;
break;
case nameof(SkiaView.ScaleY):
view.ScaleY = value;
break;
case nameof(SkiaView.Rotation):
view.Rotation = value;
break;
case nameof(SkiaView.RotationX):
view.RotationX = value;
break;
case nameof(SkiaView.RotationY):
view.RotationY = value;
break;
case nameof(SkiaView.TranslationX):
view.TranslationX = value;
break;
case nameof(SkiaView.TranslationY):
view.TranslationY = value;
break;
}
}
private static double GetProperty(SkiaView view, string propertyName)
{
return propertyName switch
{
nameof(SkiaView.Opacity) => view.Opacity,
nameof(SkiaView.Scale) => view.Scale,
nameof(SkiaView.ScaleX) => view.ScaleX,
nameof(SkiaView.ScaleY) => view.ScaleY,
nameof(SkiaView.Rotation) => view.Rotation,
nameof(SkiaView.RotationX) => view.RotationX,
nameof(SkiaView.RotationY) => view.RotationY,
nameof(SkiaView.TranslationX) => view.TranslationX,
nameof(SkiaView.TranslationY) => view.TranslationY,
_ => 0.0
};
}
public static Task<bool> AnimateAsync(
SkiaView view,
string propertyName,
double targetValue,
uint length = 250,
Easing? easing = null,
CancellationToken cancellationToken = default)
{
CancelAnimation(view, propertyName);
var animation = new RunningAnimation
{
View = view,
PropertyName = propertyName,
StartValue = GetProperty(view, propertyName),
EndValue = targetValue,
StartTime = DateTime.UtcNow,
Duration = length,
Easing = easing ?? Easing.Linear,
Completion = new TaskCompletionSource<bool>(),
Token = cancellationToken
};
_animations.Add(animation);
EnsureRunning();
return animation.Completion.Task;
}
public static void CancelAnimation(SkiaView view, string propertyName)
{
var animation = _animations.FirstOrDefault(a => a.View == view && a.PropertyName == propertyName);
if (animation != null)
{
_animations.Remove(animation);
animation.Completion.TrySetResult(false);
}
}
public static void CancelAnimations(SkiaView view)
{
foreach (var animation in _animations.Where(a => a.View == view).ToList())
{
_animations.Remove(animation);
animation.Completion.TrySetResult(false);
}
}
}

View File

@@ -1,58 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
Version numbers are aligned with .NET / MAUI versions (e.g., OpenMaui 9.0.x targets .NET 9 / MAUI 9).
## [9.0.40] - 2026-03-07
> Version aligned with MAUI 9.0.40. Previously released as 1.0.0.
### Added
- 35+ Skia-rendered controls: Button, Label, Entry, Editor, CheckBox, Switch, RadioButton, Slider, Stepper, Picker, DatePicker, TimePicker, SearchBar, Image, ImageButton, ProgressBar, ActivityIndicator, BoxView, Border, Frame, ScrollView, CollectionView, CarouselView, IndicatorView, SwipeView, RefreshView, GraphicsView, WebView, MenuBar
- Navigation: NavigationPage, TabbedPage, FlyoutPage, Shell
- Full XAML support with BindableProperty for all controls
- Visual State Manager integration (Normal, PointerOver, Pressed, Focused, Disabled)
- Data binding (OneWay, TwoWay, OneTime) with IValueConverter support
- XAML styles, StaticResource, DynamicResource, merged ResourceDictionaries
- X11 display server support with full input handling
- Wayland support with XWayland fallback
- SkiaSharp hardware-accelerated rendering with dirty region optimization
- AT-SPI2 accessibility support (screen reader integration)
- High contrast mode detection and color palette support
- Input method support (IBus, Fcitx5, XIM)
- HiDPI automatic scale factor detection (GNOME, KDE, X11)
- Platform services: Clipboard, FilePicker, FolderPicker, Notifications, GlobalHotkeys, DragDrop, Launcher, Share, SecureStorage, Preferences, Browser, Email, SystemTray, VersionTracking, AppActions
- Gesture recognition: Tap, Pan, Swipe, Pinch, Pointer, Drag/Drop
- Project templates: `openmaui-linux` (code-based) and `openmaui-linux-xaml` (XAML-based)
- Visual Studio extension with project templates and launch profiles
- DiagnosticLog centralized logging infrastructure (conditional on DEBUG builds)
- Configurable gesture thresholds (SwipeMinDistance, SwipeMaxTime, etc.)
- Exception-safe rendering pipeline
- SafeHandle wrappers for native interop (GTK, X11, GObject)
- Performance benchmarks for rendering pipeline (541 passing tests)
- Threading model and DI migration documentation
### Fixed
- Native resource leaks: GTK signal disconnection, X11 cursor freeing, CSS provider unref, WebKit dlclose
- 27 empty catch blocks replaced with DiagnosticLog for debuggability
- GestureManager memory leak (view tracking dictionaries now cleaned up on dispose)
- Text binding recursion guard in EntryHandler
- Rendering pipeline crash protection (exceptions in view Draw no longer crash the app)
## [1.0.0] - 2026-03-06 [DEPRECATED]
> Superseded by 9.0.40. Identical codebase, version renumbered to align with MAUI versioning.
## [1.0.0-rc.1] - 2026-02-01
### Added
- 100% .NET MAUI API compliance - all public APIs use MAUI types
- 217 passing unit tests
## [1.0.0-preview.1] - 2025-06-01
### Added
- Initial preview release
- Core rendering engine
- Basic control set

View File

@@ -1,6 +1,6 @@
# Contributing to .NET MAUI Linux Platform # Contributing to .NET MAUI Linux Platform
Thank you for your interest in contributing to the .NET MAUI Linux Platform! This project is developed and maintained by [MarketAlly Pte Ltd](https://marketally.sg) under the leadership of David H. Friedel Jr. Thank you for your interest in contributing to the .NET MAUI Linux Platform! This project is developed and maintained by [MarketAlly LLC](https://marketally.com) under the leadership of David H. Friedel Jr.
This document provides guidelines and information for contributors. This document provides guidelines and information for contributors.

View File

@@ -1,39 +0,0 @@
// 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;
namespace Microsoft.Maui.Controls;
/// <summary>
/// Provides attached properties for Entry controls.
/// </summary>
public static class EntryExtensions
{
/// <summary>
/// Attached property for SelectAllOnDoubleClick behavior.
/// When true, double-clicking the entry selects all text instead of just the word.
/// </summary>
public static readonly BindableProperty SelectAllOnDoubleClickProperty =
BindableProperty.CreateAttached(
"SelectAllOnDoubleClick",
typeof(bool),
typeof(EntryExtensions),
false);
/// <summary>
/// Gets the SelectAllOnDoubleClick value for the specified entry.
/// </summary>
public static bool GetSelectAllOnDoubleClick(BindableObject view)
{
return (bool)view.GetValue(SelectAllOnDoubleClickProperty);
}
/// <summary>
/// Sets the SelectAllOnDoubleClick value for the specified entry.
/// </summary>
public static void SetSelectAllOnDoubleClick(BindableObject view, bool value)
{
view.SetValue(SelectAllOnDoubleClickProperty, value);
}
}

View File

@@ -1,20 +0,0 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public static class ColorExtensions
{
public static SKColor ToSKColor(this Color color)
{
return SKColorTypeConverter.ToSKColor(color);
}
public static Color ToMauiColor(this SKColor color)
{
return SKColorTypeConverter.ToMauiColor(color);
}
}

View File

@@ -235,3 +235,25 @@ public class SKColorTypeConverter : TypeConverter
} }
} }
} }
/// <summary>
/// Extension methods for color conversion.
/// </summary>
public static class ColorExtensions
{
/// <summary>
/// Converts a MAUI Color to an SKColor.
/// </summary>
public static SKColor ToSKColor(this Color color)
{
return SKColorTypeConverter.ToSKColor(color);
}
/// <summary>
/// Converts an SKColor to a MAUI Color.
/// </summary>
public static Color ToMauiColor(this SKColor color)
{
return SKColorTypeConverter.ToMauiColor(color);
}
}

View File

@@ -1,75 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public class SKPointTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || sourceType == typeof(Point) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) || destinationType == typeof(Point) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
if (value is string str)
{
return ParsePoint(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKPoint point)
{
if (destinationType == typeof(string))
{
return $"{point.X},{point.Y}";
}
if (destinationType == typeof(Point))
{
return new Point(point.X, point.Y);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKPoint ParsePoint(string str)
{
if (string.IsNullOrWhiteSpace(str))
{
return SKPoint.Empty;
}
str = str.Trim();
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 &&
float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y))
{
return new SKPoint(x, y);
}
return SKPoint.Empty;
}
}

View File

@@ -137,3 +137,192 @@ public class SKRectTypeConverter : TypeConverter
return SKRect.Empty; return SKRect.Empty;
} }
} }
/// <summary>
/// Type converter for SKSize.
/// </summary>
public class SKSizeTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Size) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Size) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
if (value is string str)
{
return ParseSize(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKSize size)
{
if (destinationType == typeof(string))
{
return $"{size.Width},{size.Height}";
}
if (destinationType == typeof(Size))
{
return new Size(size.Width, size.Height);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKSize ParseSize(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKSize.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ', 'x', 'X' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform))
{
return new SKSize(uniform, uniform);
}
}
else if (parts.Length == 2)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var height))
{
return new SKSize(width, height);
}
}
return SKSize.Empty;
}
}
/// <summary>
/// Type converter for SKPoint.
/// </summary>
public class SKPointTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Point) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Point) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
if (value is string str)
{
return ParsePoint(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKPoint point)
{
if (destinationType == typeof(string))
{
return $"{point.X},{point.Y}";
}
if (destinationType == typeof(Point))
{
return new Point(point.X, point.Y);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKPoint ParsePoint(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKPoint.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y))
{
return new SKPoint(x, y);
}
}
return SKPoint.Empty;
}
}
/// <summary>
/// Extension methods for SkiaSharp type conversions.
/// </summary>
public static class SKTypeExtensions
{
public static SKRect ToSKRect(this Thickness thickness)
{
return SKRectTypeConverter.ThicknessToSKRect(thickness);
}
public static Thickness ToThickness(this SKRect rect)
{
return SKRectTypeConverter.SKRectToThickness(rect);
}
public static SKSize ToSKSize(this Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
public static Size ToSize(this SKSize size)
{
return new Size(size.Width, size.Height);
}
public static SKPoint ToSKPoint(this Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
public static Point ToPoint(this SKPoint point)
{
return new Point(point.X, point.Y);
}
}

View File

@@ -1,82 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public class SKSizeTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || sourceType == typeof(Size) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) || destinationType == typeof(Size) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
if (value is string str)
{
return ParseSize(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKSize size)
{
if (destinationType == typeof(string))
{
return $"{size.Width},{size.Height}";
}
if (destinationType == typeof(Size))
{
return new Size(size.Width, size.Height);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKSize ParseSize(string str)
{
if (string.IsNullOrWhiteSpace(str))
{
return SKSize.Empty;
}
str = str.Trim();
var parts = str.Split(new[] { ',', ' ', 'x', 'X' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var single))
{
return new SKSize(single, single);
}
}
else if (parts.Length == 2 &&
float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var height))
{
return new SKSize(width, height);
}
return SKSize.Empty;
}
}

View File

@@ -1,40 +0,0 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public static class SKTypeExtensions
{
public static SKRect ToSKRect(this Thickness thickness)
{
return SKRectTypeConverter.ThicknessToSKRect(thickness);
}
public static Thickness ToThickness(this SKRect rect)
{
return SKRectTypeConverter.SKRectToThickness(rect);
}
public static SKSize ToSKSize(this Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
public static Size ToSize(this SKSize size)
{
return new Size(size.Width, size.Height);
}
public static SKPoint ToSKPoint(this Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
public static Point ToPoint(this SKPoint point)
{
return new Point(point.X, point.Y);
}
}

View File

@@ -1,77 +0,0 @@
using System;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Dispatching;
public class LinuxDispatcher : IDispatcher
{
private static int _mainThreadId;
private static LinuxDispatcher? _mainDispatcher;
private static readonly object _lock = new object();
public static LinuxDispatcher? Main => _mainDispatcher;
public static bool IsMainThread => Environment.CurrentManagedThreadId == _mainThreadId;
public bool IsDispatchRequired => !IsMainThread;
public static void Initialize()
{
lock (_lock)
{
_mainThreadId = Environment.CurrentManagedThreadId;
_mainDispatcher = new LinuxDispatcher();
DiagnosticLog.Debug("LinuxDispatcher", $"Initialized on thread {_mainThreadId}");
}
}
public bool Dispatch(Action action)
{
ArgumentNullException.ThrowIfNull(action, "action");
if (!IsDispatchRequired)
{
action();
return true;
}
GLibNative.IdleAdd(delegate
{
try
{
action();
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxDispatcher", "Error in dispatched action", ex);
}
return false;
});
return true;
}
public bool DispatchDelayed(TimeSpan delay, Action action)
{
ArgumentNullException.ThrowIfNull(action, "action");
GLibNative.TimeoutAdd((uint)Math.Max(0.0, delay.TotalMilliseconds), delegate
{
try
{
action();
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxDispatcher", "Error in delayed action", ex);
}
return false;
});
return true;
}
public IDispatcherTimer CreateTimer()
{
return new LinuxDispatcherTimer(this);
}
}

View File

@@ -1,15 +0,0 @@
using Microsoft.Maui.Dispatching;
namespace Microsoft.Maui.Platform.Linux.Dispatching;
public class LinuxDispatcherProvider : IDispatcherProvider
{
private static LinuxDispatcherProvider? _instance;
public static LinuxDispatcherProvider Instance => _instance ?? (_instance = new LinuxDispatcherProvider());
public IDispatcher? GetForCurrentThread()
{
return LinuxDispatcher.Main;
}
}

View File

@@ -1,110 +0,0 @@
using System;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Dispatching;
public class LinuxDispatcherTimer : IDispatcherTimer
{
private readonly LinuxDispatcher _dispatcher;
private uint _sourceId;
private TimeSpan _interval = TimeSpan.FromMilliseconds(100);
private bool _isRepeating = true;
private bool _isRunning;
public TimeSpan Interval
{
get
{
return _interval;
}
set
{
_interval = value;
if (_isRunning)
{
Stop();
Start();
}
}
}
public bool IsRepeating
{
get
{
return _isRepeating;
}
set
{
_isRepeating = value;
}
}
public bool IsRunning => _isRunning;
public event EventHandler? Tick;
public LinuxDispatcherTimer(LinuxDispatcher dispatcher)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
public void Start()
{
if (!_isRunning)
{
_isRunning = true;
ScheduleNext();
}
}
public void Stop()
{
if (_isRunning)
{
_isRunning = false;
if (_sourceId != 0)
{
GLibNative.SourceRemove(_sourceId);
_sourceId = 0;
}
}
}
private void ScheduleNext()
{
if (!_isRunning)
{
return;
}
uint intervalMs = (uint)Math.Max(1.0, _interval.TotalMilliseconds);
_sourceId = GLibNative.TimeoutAdd(intervalMs, delegate
{
if (!_isRunning)
{
return false;
}
try
{
Tick?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxDispatcherTimer", "Error in Tick handler", ex);
}
if (_isRepeating && _isRunning)
{
return true;
}
_isRunning = false;
_sourceId = 0;
return false;
});
}
}

View File

@@ -1,11 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux;
public enum DisplayServerType
{
Auto,
X11,
Wayland
}

View File

@@ -1,53 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux;
public class Easing
{
private readonly Func<double, double> _easingFunc;
public static readonly Easing Linear = new(v => v);
public static readonly Easing SinIn = new(v => 1.0 - Math.Cos(v * Math.PI / 2.0));
public static readonly Easing SinOut = new(v => Math.Sin(v * Math.PI / 2.0));
public static readonly Easing SinInOut = new(v => -(Math.Cos(Math.PI * v) - 1.0) / 2.0);
public static readonly Easing CubicIn = new(v => v * v * v);
public static readonly Easing CubicOut = new(v => 1.0 - Math.Pow(1.0 - v, 3.0));
public static readonly Easing CubicInOut = new(v =>
v < 0.5 ? 4.0 * v * v * v : 1.0 - Math.Pow(-2.0 * v + 2.0, 3.0) / 2.0);
// BounceOut must be declared before BounceIn since BounceIn references it
public static readonly Easing BounceOut = new(v =>
{
const double n1 = 7.5625;
const double d1 = 2.75;
if (v < 1 / d1)
return n1 * v * v;
if (v < 2 / d1)
return n1 * (v -= 1.5 / d1) * v + 0.75;
if (v < 2.5 / d1)
return n1 * (v -= 2.25 / d1) * v + 0.9375;
return n1 * (v -= 2.625 / d1) * v + 0.984375;
});
public static readonly Easing BounceIn = new(v => 1.0 - BounceOut.Ease(1.0 - v));
public static readonly Easing SpringIn = new(v => v * v * (2.70158 * v - 1.70158));
public static readonly Easing SpringOut = new(v =>
(v - 1.0) * (v - 1.0) * (2.70158 * (v - 1.0) + 1.70158) + 1.0);
public Easing(Func<double, double> easingFunc)
{
_easingFunc = easingFunc;
}
public double Ease(double v) => _easingFunc(v);
}

View File

@@ -0,0 +1,63 @@
// 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;
/// <summary>
/// Linux handler for ActivityIndicator control.
/// </summary>
public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator, SkiaActivityIndicator>
{
public static IPropertyMapper<IActivityIndicator, ActivityIndicatorHandler> Mapper = new PropertyMapper<IActivityIndicator, ActivityIndicatorHandler>(ViewHandler.ViewMapper)
{
[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);
public ActivityIndicatorHandler() : base(Mapper, CommandMapper) { }
protected override SkiaActivityIndicator CreatePlatformView() => new SkiaActivityIndicator();
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
handler.PlatformView.IsRunning = activityIndicator.IsRunning;
}
public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator.Color != null)
handler.PlatformView.Color = activityIndicator.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
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

@@ -3,7 +3,7 @@
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -18,7 +18,6 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning, [nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
[nameof(IActivityIndicator.Color)] = MapColor, [nameof(IActivityIndicator.Color)] = MapColor,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
}; };
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -39,19 +38,6 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
return new SkiaActivityIndicator(); return new SkiaActivityIndicator();
} }
protected override void ConnectHandler(SkiaActivityIndicator platformView)
{
base.ConnectHandler(platformView);
// Sync properties
if (VirtualView != null)
{
MapIsRunning(this, VirtualView);
MapColor(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
@@ -63,7 +49,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (activityIndicator.Color is not null) if (activityIndicator.Color is not null)
handler.PlatformView.Color = activityIndicator.Color; handler.PlatformView.Color = activityIndicator.Color.ToSKColor();
} }
public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
@@ -72,14 +58,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
public static void MapIsEnabled(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = activityIndicator.IsEnabled;
handler.PlatformView.Invalidate();
}
} }

View File

@@ -5,8 +5,6 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -22,17 +20,10 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
[nameof(IBorderView.Content)] = MapContent, [nameof(IBorderView.Content)] = MapContent,
[nameof(IBorderStroke.Stroke)] = MapStroke, [nameof(IBorderStroke.Stroke)] = MapStroke,
[nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness, [nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness,
["StrokeDashArray"] = MapStrokeDashArray,
["StrokeDashOffset"] = MapStrokeDashOffset,
[nameof(IBorderStroke.StrokeLineCap)] = MapStrokeLineCap,
[nameof(IBorderStroke.StrokeLineJoin)] = MapStrokeLineJoin,
[nameof(IBorderStroke.StrokeMiterLimit)] = MapStrokeMiterLimit,
["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke ["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor, ["BackgroundColor"] = MapBackgroundColor,
[nameof(IPadding.Padding)] = MapPadding, [nameof(IPadding.Padding)] = MapPadding,
["WidthRequest"] = MapWidthRequest,
["HeightRequest"] = MapHeightRequest,
}; };
public static CommandMapper<IBorderView, BorderHandler> CommandMapper = public static CommandMapper<IBorderView, BorderHandler> CommandMapper =
@@ -57,49 +48,13 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
protected override void ConnectHandler(SkiaBorder platformView) protected override void ConnectHandler(SkiaBorder platformView)
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
if (VirtualView is View view)
{
platformView.MauiView = view;
}
platformView.Tapped += OnPlatformViewTapped;
// Explicitly map properties since they may be set before handler creation
if (VirtualView is VisualElement ve)
{
if (ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor;
}
else if (ve.Background is SolidColorBrush brush && brush.Color != null)
{
platformView.BackgroundColor = brush.Color;
}
if (ve.WidthRequest >= 0)
{
platformView.WidthRequest = ve.WidthRequest;
}
if (ve.HeightRequest >= 0)
{
platformView.HeightRequest = ve.HeightRequest;
}
}
} }
protected override void DisconnectHandler(SkiaBorder platformView) protected override void DisconnectHandler(SkiaBorder platformView)
{ {
platformView.Tapped -= OnPlatformViewTapped;
platformView.MauiView = null;
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnPlatformViewTapped(object? sender, EventArgs e)
{
if (VirtualView is View view)
{
GestureManager.ProcessTap(view, 0.0, 0.0);
}
}
public static void MapContent(BorderHandler handler, IBorderView border) public static void MapContent(BorderHandler handler, IBorderView border)
{ {
if (handler.PlatformView is null || handler.MauiContext is null) return; if (handler.PlatformView is null || handler.MauiContext is null) return;
@@ -112,13 +67,13 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
// Create handler for content if it doesn't exist // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
DiagnosticLog.Debug("BorderHandler", $"Creating handler for content: {content.GetType().Name}"); Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToViewHandler(handler.MauiContext); content.Handler = content.ToHandler(handler.MauiContext);
} }
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)
{ {
DiagnosticLog.Debug("BorderHandler", $"Adding content: {skiaContent.GetType().Name}"); Console.WriteLine($"[BorderHandler] Adding content: {skiaContent.GetType().Name}");
handler.PlatformView.AddChild(skiaContent); handler.PlatformView.AddChild(skiaContent);
} }
} }
@@ -130,14 +85,14 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null) if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.Stroke = solidPaint.Color; handler.PlatformView.Stroke = solidPaint.Color.ToSKColor();
} }
} }
public static void MapStrokeThickness(BorderHandler handler, IBorderView border) public static void MapStrokeThickness(BorderHandler handler, IBorderView border)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.StrokeThickness = border.StrokeThickness; handler.PlatformView.StrokeThickness = (float)border.StrokeThickness;
} }
public static void MapBackground(BorderHandler handler, IBorderView border) public static void MapBackground(BorderHandler handler, IBorderView border)
@@ -146,7 +101,7 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -154,27 +109,22 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (border is VisualElement ve) if (border is VisualElement ve && ve.BackgroundColor != null)
{ {
var bgColor = ve.BackgroundColor; handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
DiagnosticLog.Debug("BorderHandler", $"MapBackgroundColor: {bgColor}");
if (bgColor != null)
{
handler.PlatformView.BackgroundColor = bgColor;
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
} }
}
public static void MapPadding(BorderHandler handler, IBorderView border) public static void MapPadding(BorderHandler handler, IBorderView border)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
var padding = border.Padding; var padding = border.Padding;
handler.PlatformView.PaddingLeft = padding.Left; handler.PlatformView.PaddingLeft = (float)padding.Left;
handler.PlatformView.PaddingTop = padding.Top; handler.PlatformView.PaddingTop = (float)padding.Top;
handler.PlatformView.PaddingRight = padding.Right; handler.PlatformView.PaddingRight = (float)padding.Right;
handler.PlatformView.PaddingBottom = padding.Bottom; handler.PlatformView.PaddingBottom = (float)padding.Bottom;
} }
public static void MapStrokeShape(BorderHandler handler, IBorderView border) public static void MapStrokeShape(BorderHandler handler, IBorderView border)
@@ -185,109 +135,24 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
if (border is not Border borderControl) return; if (border is not Border borderControl) return;
var shape = borderControl.StrokeShape; var shape = borderControl.StrokeShape;
// Pass the shape directly to the platform view for full shape support
handler.PlatformView.StrokeShape = shape;
// Also set CornerRadius for backward compatibility when StrokeShape is RoundRectangle
if (shape is Microsoft.Maui.Controls.Shapes.RoundRectangle roundRect) 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; var cornerRadius = roundRect.CornerRadius;
handler.PlatformView.CornerRadius = cornerRadius.TopLeft; handler.PlatformView.CornerRadius = (float)cornerRadius.TopLeft;
} }
else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle) else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle)
{ {
handler.PlatformView.CornerRadius = 0.0; handler.PlatformView.CornerRadius = 0;
} }
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse) else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
{ {
handler.PlatformView.CornerRadius = double.MaxValue; // 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(); handler.PlatformView.Invalidate();
} }
public static void MapStrokeDashArray(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
// StrokeDashArray is on Border class
if (border is Border borderControl && borderControl.StrokeDashArray != null)
{
var dashArray = new DoubleCollection();
foreach (var value in borderControl.StrokeDashArray)
{
dashArray.Add(value);
}
handler.PlatformView.StrokeDashArray = dashArray;
}
handler.PlatformView.Invalidate();
}
public static void MapStrokeDashOffset(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
// StrokeDashOffset is on Border class
if (border is Border borderControl)
{
handler.PlatformView.StrokeDashOffset = borderControl.StrokeDashOffset;
}
handler.PlatformView.Invalidate();
}
public static void MapStrokeLineCap(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is IBorderStroke borderStroke)
{
handler.PlatformView.StrokeLineCap = borderStroke.StrokeLineCap;
}
handler.PlatformView.Invalidate();
}
public static void MapStrokeLineJoin(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is IBorderStroke borderStroke)
{
handler.PlatformView.StrokeLineJoin = borderStroke.StrokeLineJoin;
}
handler.PlatformView.Invalidate();
}
public static void MapStrokeMiterLimit(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is IBorderStroke borderStroke)
{
handler.PlatformView.StrokeMiterLimit = borderStroke.StrokeMiterLimit;
}
handler.PlatformView.Invalidate();
}
public static void MapWidthRequest(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.WidthRequest >= 0)
{
handler.PlatformView.WidthRequest = ve.WidthRequest;
handler.PlatformView.InvalidateMeasure();
}
}
public static void MapHeightRequest(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.HeightRequest >= 0)
{
handler.PlatformView.HeightRequest = ve.HeightRequest;
handler.PlatformView.InvalidateMeasure();
}
}
} }

View File

@@ -30,38 +30,28 @@ public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
return new SkiaBoxView(); return new SkiaBoxView();
} }
protected override void ConnectHandler(SkiaBoxView platformView)
{
base.ConnectHandler(platformView);
// Map size requests from MAUI BoxView
if (VirtualView is BoxView boxView)
{
if (boxView.WidthRequest >= 0)
platformView.WidthRequest = boxView.WidthRequest;
if (boxView.HeightRequest >= 0)
platformView.HeightRequest = boxView.HeightRequest;
}
}
public static void MapColor(BoxViewHandler handler, BoxView boxView) public static void MapColor(BoxViewHandler handler, BoxView boxView)
{ {
if (boxView.Color != null) if (boxView.Color != null)
{ {
handler.PlatformView.Color = boxView.Color; 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) public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView)
{ {
handler.PlatformView.CornerRadius = boxView.CornerRadius; handler.PlatformView.CornerRadius = (float)boxView.CornerRadius.TopLeft;
} }
public static void MapBackground(BoxViewHandler handler, BoxView boxView) public static void MapBackground(BoxViewHandler handler, BoxView boxView)
{ {
if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null) if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
} }
@@ -70,7 +60,7 @@ public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
{ {
if (boxView.BackgroundColor != null) if (boxView.BackgroundColor != null)
{ {
handler.PlatformView.BackgroundColor = boxView.BackgroundColor; handler.PlatformView.BackgroundColor = boxView.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
} }

View File

@@ -0,0 +1,179 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for Button control.
/// </summary>
public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
{
/// <summary>
/// Maps the property mapper for the handler.
/// </summary>
public static IPropertyMapper<IButton, ButtonHandler> Mapper = new PropertyMapper<IButton, ButtonHandler>(ViewHandler.ViewMapper)
{
[nameof(IButton.Text)] = MapText,
[nameof(IButton.TextColor)] = MapTextColor,
[nameof(IButton.Background)] = MapBackground,
[nameof(IButton.Font)] = MapFont,
[nameof(IButton.Padding)] = MapPadding,
[nameof(IButton.CornerRadius)] = MapCornerRadius,
[nameof(IButton.BorderColor)] = MapBorderColor,
[nameof(IButton.BorderWidth)] = MapBorderWidth,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
public ButtonHandler() : base(Mapper, CommandMapper)
{
}
public ButtonHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaButton CreatePlatformView()
{
var button = new SkiaButton();
return button;
}
protected override void ConnectHandler(SkiaButton platformView)
{
base.ConnectHandler(platformView);
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)
{
platformView.Clicked -= OnClicked;
platformView.Pressed -= OnPressed;
platformView.Released -= OnReleased;
base.DisconnectHandler(platformView);
}
private void OnClicked(object? sender, EventArgs e)
{
VirtualView?.Clicked();
}
private void OnPressed(object? sender, EventArgs e)
{
VirtualView?.Pressed();
}
private void OnReleased(object? sender, EventArgs e)
{
VirtualView?.Released();
}
public static void MapText(ButtonHandler handler, IButton button)
{
handler.PlatformView.Text = button.Text ?? "";
handler.PlatformView.Invalidate();
}
public static void MapTextColor(ButtonHandler handler, IButton button)
{
if (button.TextColor != null)
{
handler.PlatformView.TextColor = button.TextColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapBackground(ButtonHandler handler, IButton button)
{
var background = button.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
// Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = solidBrush.Color.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapFont(ButtonHandler handler, IButton button)
{
var font = button.Font;
if (font.Family != null)
{
handler.PlatformView.FontFamily = font.Family;
}
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.IsBold = font.Weight == FontWeight.Bold;
handler.PlatformView.Invalidate();
}
public static void MapPadding(ButtonHandler handler, IButton button)
{
var padding = button.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.Invalidate();
}
public static void MapCornerRadius(ButtonHandler handler, IButton button)
{
handler.PlatformView.CornerRadius = button.CornerRadius;
handler.PlatformView.Invalidate();
}
public static void MapBorderColor(ButtonHandler handler, IButton button)
{
if (button.StrokeColor != null)
{
handler.PlatformView.BorderColor = button.StrokeColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapBorderWidth(ButtonHandler handler, IButton button)
{
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
handler.PlatformView.Invalidate();
}
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

@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -61,20 +59,6 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
MapBackground(this, VirtualView); MapBackground(this, VirtualView);
MapPadding(this, VirtualView); MapPadding(this, VirtualView);
MapIsEnabled(this, VirtualView); MapIsEnabled(this, VirtualView);
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
DiagnosticLog.Debug("ButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
else
{
DiagnosticLog.Debug("ButtonHandler", $"VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
}
} }
} }
@@ -96,13 +80,13 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
var strokeColor = button.StrokeColor; var strokeColor = button.StrokeColor;
if (strokeColor is not null) if (strokeColor is not null)
handler.PlatformView.BorderColor = strokeColor; handler.PlatformView.BorderColor = strokeColor.ToSKColor();
} }
public static void MapStrokeThickness(ButtonHandler handler, IButton button) public static void MapStrokeThickness(ButtonHandler handler, IButton button)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.BorderWidth = button.StrokeThickness; handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
} }
public static void MapCornerRadius(ButtonHandler handler, IButton button) public static void MapCornerRadius(ButtonHandler handler, IButton button)
@@ -117,8 +101,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
// Set BackgroundColor (MAUI Color type) // Set ButtonBackgroundColor (used for rendering) not base BackgroundColor
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -127,16 +111,17 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
var padding = button.Padding; var padding = button.Padding;
handler.PlatformView.Padding = new Thickness( handler.PlatformView.Padding = new SKRect(
padding.Left, (float)padding.Left,
padding.Top, (float)padding.Top,
padding.Right, (float)padding.Right,
padding.Bottom); (float)padding.Bottom);
} }
public static void MapIsEnabled(ButtonHandler handler, IButton button) public static void MapIsEnabled(ButtonHandler handler, IButton button)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled; handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
@@ -163,7 +148,6 @@ public partial class TextButtonHandler : ButtonHandler
protected override void ConnectHandler(SkiaButton platformView) protected override void ConnectHandler(SkiaButton platformView)
{ {
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler START");
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
// Manually map text properties on connect since MAUI may not trigger updates // Manually map text properties on connect since MAUI may not trigger updates
@@ -175,17 +159,6 @@ public partial class TextButtonHandler : ButtonHandler
MapFont(this, textButton); MapFont(this, textButton);
MapCharacterSpacing(this, textButton); MapCharacterSpacing(this, textButton);
} }
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
DiagnosticLog.Debug("TextButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler DONE");
} }
public static void MapText(TextButtonHandler handler, ITextButton button) public static void MapText(TextButtonHandler handler, ITextButton button)
@@ -199,7 +172,7 @@ public partial class TextButtonHandler : ButtonHandler
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (button.TextColor is not null) if (button.TextColor is not null)
handler.PlatformView.TextColor = button.TextColor; handler.PlatformView.TextColor = button.TextColor.ToSKColor();
} }
public static void MapFont(TextButtonHandler handler, ITextButton button) public static void MapFont(TextButtonHandler handler, ITextButton button)
@@ -208,23 +181,18 @@ public partial class TextButtonHandler : ButtonHandler
var font = button.Font; var font = button.Font;
if (font.Size > 0) if (font.Size > 0)
handler.PlatformView.FontSize = font.Size; handler.PlatformView.FontSize = (float)font.Size;
if (!string.IsNullOrEmpty(font.Family)) if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family; handler.PlatformView.FontFamily = font.Family;
// Convert Font weight/slant to FontAttributes handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
FontAttributes attrs = FontAttributes.None; handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
} }
public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button) public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = button.CharacterSpacing; handler.PlatformView.CharacterSpacing = (float)button.CharacterSpacing;
} }
} }

View File

@@ -1,255 +0,0 @@
// 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.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for CarouselView on Linux using Skia rendering.
/// Maps CarouselView to SkiaCarouselView platform view.
/// </summary>
public partial class CarouselViewHandler : ViewHandler<CarouselView, SkiaCarouselView>
{
private bool _isUpdatingPosition;
public static IPropertyMapper<CarouselView, CarouselViewHandler> Mapper =
new PropertyMapper<CarouselView, CarouselViewHandler>(ViewHandler.ViewMapper)
{
// ItemsView properties
[nameof(ItemsView.ItemsSource)] = MapItemsSource,
[nameof(ItemsView.ItemTemplate)] = MapItemTemplate,
[nameof(ItemsView.EmptyView)] = MapEmptyView,
// CarouselView specific properties
[nameof(CarouselView.Position)] = MapPosition,
[nameof(CarouselView.CurrentItem)] = MapCurrentItem,
[nameof(CarouselView.IsBounceEnabled)] = MapIsBounceEnabled,
[nameof(CarouselView.IsSwipeEnabled)] = MapIsSwipeEnabled,
[nameof(CarouselView.Loop)] = MapLoop,
[nameof(CarouselView.PeekAreaInsets)] = MapPeekAreaInsets,
[nameof(IView.Background)] = MapBackground,
};
public static CommandMapper<CarouselView, CarouselViewHandler> CommandMapper =
new(ViewHandler.ViewCommandMapper)
{
["ScrollTo"] = MapScrollTo,
};
public CarouselViewHandler() : base(Mapper, CommandMapper)
{
}
public CarouselViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaCarouselView CreatePlatformView()
{
return new SkiaCarouselView();
}
protected override void ConnectHandler(SkiaCarouselView platformView)
{
base.ConnectHandler(platformView);
platformView.PositionChanged += OnPositionChanged;
platformView.Scrolled += OnScrolled;
}
protected override void DisconnectHandler(SkiaCarouselView platformView)
{
platformView.PositionChanged -= OnPositionChanged;
platformView.Scrolled -= OnScrolled;
base.DisconnectHandler(platformView);
}
private void OnPositionChanged(object? sender, PositionChangedEventArgs e)
{
if (VirtualView is null || _isUpdatingPosition) return;
try
{
_isUpdatingPosition = true;
if (VirtualView.Position != e.CurrentPosition)
{
VirtualView.Position = e.CurrentPosition;
}
// Update CurrentItem
if (VirtualView.ItemsSource is System.Collections.IList list &&
e.CurrentPosition >= 0 && e.CurrentPosition < list.Count)
{
VirtualView.CurrentItem = list[e.CurrentPosition];
}
}
finally
{
_isUpdatingPosition = false;
}
}
private void OnScrolled(object? sender, EventArgs e)
{
// CarouselView doesn't have a direct Scrolled event in MAUI
// but we can use this for internal state tracking
}
public static void MapItemsSource(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
handler.PlatformView.ClearItems();
var itemsSource = carouselView.ItemsSource;
if (itemsSource == null) return;
var template = carouselView.ItemTemplate;
foreach (var item in itemsSource)
{
SkiaView? skiaView = null;
if (template != null)
{
try
{
var content = template.CreateContent();
if (content is View view)
{
view.BindingContext = item;
if (view.Handler == null)
{
view.Handler = view.ToViewHandler(handler.MauiContext);
}
if (view.Handler?.PlatformView is SkiaView sv)
{
skiaView = sv;
}
}
}
catch
{
// Ignore template errors
}
}
if (skiaView == null)
{
// Create a simple label for the item
skiaView = new SkiaLabel { Text = item?.ToString() ?? "" };
}
handler.PlatformView.AddItem(skiaView);
}
handler.PlatformView.Invalidate();
}
public static void MapItemTemplate(CarouselViewHandler handler, CarouselView carouselView)
{
// Re-map items when template changes
MapItemsSource(handler, carouselView);
}
public static void MapEmptyView(CarouselViewHandler handler, CarouselView carouselView)
{
// CarouselView doesn't typically show empty view - handled by ItemsSource
}
public static void MapPosition(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null || handler._isUpdatingPosition) return;
try
{
handler._isUpdatingPosition = true;
if (handler.PlatformView.Position != carouselView.Position)
{
handler.PlatformView.Position = carouselView.Position;
}
}
finally
{
handler._isUpdatingPosition = false;
}
}
public static void MapCurrentItem(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null || handler._isUpdatingPosition) return;
// Find position of current item
if (carouselView.ItemsSource is System.Collections.IList list && carouselView.CurrentItem != null)
{
int index = list.IndexOf(carouselView.CurrentItem);
if (index >= 0 && index != handler.PlatformView.Position)
{
try
{
handler._isUpdatingPosition = true;
handler.PlatformView.Position = index;
}
finally
{
handler._isUpdatingPosition = false;
}
}
}
}
public static void MapIsBounceEnabled(CarouselViewHandler handler, CarouselView carouselView)
{
// SkiaCarouselView handles bounce internally
// Could add IsBounceEnabled property if needed
}
public static void MapIsSwipeEnabled(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsSwipeEnabled = carouselView.IsSwipeEnabled;
}
public static void MapLoop(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Loop = carouselView.Loop;
}
public static void MapPeekAreaInsets(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null) return;
// PeekAreaInsets is a Thickness in MAUI, use Left for horizontal peek
handler.PlatformView.PeekAreaInsets = (float)carouselView.PeekAreaInsets.Left;
}
public static void MapBackground(CarouselViewHandler handler, CarouselView carouselView)
{
if (handler.PlatformView is null) return;
if (carouselView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
public static void MapScrollTo(CarouselViewHandler handler, CarouselView carouselView, object? args)
{
if (handler.PlatformView is null) return;
if (args is ScrollToRequestEventArgs scrollArgs)
{
handler.PlatformView.ScrollTo(scrollArgs.Index, scrollArgs.IsAnimated);
}
}
}

View File

@@ -0,0 +1,113 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for CheckBox control.
/// </summary>
public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
{
/// <summary>
/// Maps the property mapper for the handler.
/// </summary>
public static IPropertyMapper<ICheckBox, CheckBoxHandler> Mapper = new PropertyMapper<ICheckBox, CheckBoxHandler>(ViewHandler.ViewMapper)
{
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
public CheckBoxHandler() : base(Mapper, CommandMapper)
{
}
public CheckBoxHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaCheckBox CreatePlatformView()
{
return new SkiaCheckBox();
}
protected override void ConnectHandler(SkiaCheckBox platformView)
{
base.ConnectHandler(platformView);
platformView.CheckedChanged += OnCheckedChanged;
}
protected override void DisconnectHandler(SkiaCheckBox platformView)
{
platformView.CheckedChanged -= OnCheckedChanged;
base.DisconnectHandler(platformView);
}
private void OnCheckedChanged(object? sender, CheckedChangedEventArgs e)
{
if (VirtualView != null && VirtualView.IsChecked != e.IsChecked)
{
VirtualView.IsChecked = e.IsChecked;
}
}
public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView.IsChecked != checkBox.IsChecked)
{
handler.PlatformView.IsChecked = checkBox.IsChecked;
}
}
public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox)
{
var foreground = checkBox.Foreground;
if (foreground is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BoxColor = solidBrush.Color.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
{
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,7 +18,6 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
[nameof(ICheckBox.IsChecked)] = MapIsChecked, [nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground, [nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
}; };
@@ -73,7 +72,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color is not null) if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.CheckColor = solidPaint.Color; handler.PlatformView.CheckColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -83,16 +82,10 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.Color = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
}
public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;

View File

@@ -5,8 +5,6 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -125,49 +123,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e) private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e)
{ {
if (VirtualView is null || _isUpdatingSelection) return; // Item tap is handled through selection
try
{
_isUpdatingSelection = true;
DiagnosticLog.Debug("CollectionViewHandler", $"OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}");
// Try to get the item view and process gestures
var skiaView = PlatformView?.GetItemView(e.Index);
DiagnosticLog.Debug("CollectionViewHandler", $"GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? "null"}, MauiView={skiaView?.MauiView?.GetType().Name ?? "null"}");
if (skiaView?.MauiView != null)
{
DiagnosticLog.Debug("CollectionViewHandler", $"Found MauiView: {skiaView.MauiView.GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}");
if (GestureManager.ProcessTap(skiaView.MauiView, 0, 0))
{
DiagnosticLog.Debug("CollectionViewHandler", "Gesture processed successfully");
return;
}
}
// Handle selection if gesture wasn't processed
if (VirtualView.SelectionMode == SelectionMode.Single)
{
VirtualView.SelectedItem = e.Item;
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
if (VirtualView.SelectedItems.Contains(e.Item))
{
VirtualView.SelectedItems.Remove(e.Item);
}
else
{
VirtualView.SelectedItems.Add(e.Item);
}
}
}
finally
{
_isUpdatingSelection = false;
}
} }
public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView) public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView)
@@ -202,14 +158,11 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
// Create handler for the view // Create handler for the view
if (view.Handler == null && handler.MauiContext != null) if (view.Handler == null && handler.MauiContext != null)
{ {
view.Handler = view.ToViewHandler(handler.MauiContext); view.Handler = view.ToHandler(handler.MauiContext);
} }
if (view.Handler?.PlatformView is SkiaView skiaView) if (view.Handler?.PlatformView is SkiaView skiaView)
{ {
// Set MauiView so gestures can be processed
skiaView.MauiView = view;
DiagnosticLog.Debug("CollectionViewHandler", $"ItemViewCreator: Set MauiView={view.GetType().Name} on {skiaView.GetType().Name}, GestureRecognizers={view.GestureRecognizers?.Count ?? 0}");
return skiaView; return skiaView;
} }
} }
@@ -221,7 +174,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
{ {
if (cellView.Handler == null && handler.MauiContext != null) if (cellView.Handler == null && handler.MauiContext != null)
{ {
cellView.Handler = cellView.ToViewHandler(handler.MauiContext); cellView.Handler = cellView.ToHandler(handler.MauiContext);
} }
if (cellView.Handler?.PlatformView is SkiaView skiaView) if (cellView.Handler?.PlatformView is SkiaView skiaView)
@@ -362,7 +315,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
if (collectionView.Background is SolidColorBrush solidBrush) if (collectionView.Background is SolidColorBrush solidBrush)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
} }
} }
@@ -372,7 +325,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
if (collectionView.BackgroundColor is not null) if (collectionView.BackgroundColor is not null)
{ {
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor; handler.PlatformView.BackgroundColor = collectionView.BackgroundColor.ToSKColor();
} }
} }

View File

@@ -23,7 +23,6 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
[nameof(IDatePicker.Format)] = MapFormat, [nameof(IDatePicker.Format)] = MapFormat,
[nameof(IDatePicker.TextColor)] = MapTextColor, [nameof(IDatePicker.TextColor)] = MapTextColor,
[nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing, [nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
}; };
@@ -50,17 +49,6 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.DateSelected += OnDateSelected; platformView.DateSelected += OnDateSelected;
// Apply dark theme colors if dark mode is active
var current = Application.Current;
if (current != null && (int)current.UserAppTheme == 2) // Dark theme
{
platformView.CalendarBackgroundColor = Color.FromRgb(30, 30, 30);
platformView.TextColor = Color.FromRgb(224, 224, 224);
platformView.BorderColor = Color.FromRgb(97, 97, 97);
platformView.DisabledDayColor = Color.FromRgb(97, 97, 97);
platformView.BackgroundColor = Color.FromRgb(45, 45, 45);
}
} }
protected override void DisconnectHandler(SkiaDatePicker platformView) protected override void DisconnectHandler(SkiaDatePicker platformView)
@@ -69,11 +57,11 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnDateSelected(object? sender, DateChangedEventArgs e) private void OnDateSelected(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
VirtualView.Date = e.NewDate; VirtualView.Date = PlatformView.Date;
} }
public static void MapDate(DatePickerHandler handler, IDatePicker datePicker) public static void MapDate(DatePickerHandler handler, IDatePicker datePicker)
@@ -105,33 +93,13 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (datePicker.TextColor is not null) if (datePicker.TextColor is not null)
{ {
handler.PlatformView.TextColor = datePicker.TextColor; handler.PlatformView.TextColor = datePicker.TextColor.ToSKColor();
} }
} }
public static void MapCharacterSpacing(DatePickerHandler handler, IDatePicker datePicker) public static void MapCharacterSpacing(DatePickerHandler handler, IDatePicker datePicker)
{ {
if (handler.PlatformView is null) return; // Character spacing would require custom text rendering
handler.PlatformView.CharacterSpacing = datePicker.CharacterSpacing;
}
public static void MapFont(DatePickerHandler handler, IDatePicker datePicker)
{
if (handler.PlatformView is null) return;
var font = datePicker.Font;
if (font.Size > 0)
handler.PlatformView.FontSize = font.Size;
if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family;
// Map FontAttributes from the Font weight/slant
var attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
// Note: Font.Slant for italic would require checking FontSlant
handler.PlatformView.FontAttributes = attrs;
} }
public static void MapBackground(DatePickerHandler handler, IDatePicker datePicker) public static void MapBackground(DatePickerHandler handler, IDatePicker datePicker)
@@ -140,7 +108,7 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
if (datePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (datePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
} }

View File

@@ -21,11 +21,9 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
[nameof(IEditor.Placeholder)] = MapPlaceholder, [nameof(IEditor.Placeholder)] = MapPlaceholder,
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor, [nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
[nameof(IEditor.TextColor)] = MapTextColor, [nameof(IEditor.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing, [nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly, [nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled, [nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
[nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
[nameof(IEditor.MaxLength)] = MapMaxLength, [nameof(IEditor.MaxLength)] = MapMaxLength,
[nameof(IEditor.CursorPosition)] = MapCursorPosition, [nameof(IEditor.CursorPosition)] = MapCursorPosition,
[nameof(IEditor.SelectionLength)] = MapSelectionLength, [nameof(IEditor.SelectionLength)] = MapSelectionLength,
@@ -99,7 +97,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (editor.PlaceholderColor is not null) if (editor.PlaceholderColor is not null)
{ {
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor; handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor();
} }
} }
@@ -108,34 +106,13 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (editor.TextColor is not null) if (editor.TextColor is not null)
{ {
handler.PlatformView.TextColor = editor.TextColor; handler.PlatformView.TextColor = editor.TextColor.ToSKColor();
} }
} }
public static void MapFont(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView is null) return;
var font = editor.Font;
if (font.Size > 0)
handler.PlatformView.FontSize = font.Size;
if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family;
// Convert Font weight/slant to FontAttributes
FontAttributes attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
}
public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) public static void MapCharacterSpacing(EditorHandler handler, IEditor editor)
{ {
if (handler.PlatformView is null) return; // Character spacing would require custom text rendering
handler.PlatformView.CharacterSpacing = editor.CharacterSpacing;
} }
public static void MapIsReadOnly(EditorHandler handler, IEditor editor) public static void MapIsReadOnly(EditorHandler handler, IEditor editor)
@@ -146,14 +123,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor) public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor)
{ {
if (handler.PlatformView is null) return; // Text prediction not applicable to desktop
handler.PlatformView.IsTextPredictionEnabled = editor.IsTextPredictionEnabled;
}
public static void MapIsSpellCheckEnabled(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsSpellCheckEnabled = editor.IsSpellCheckEnabled;
} }
public static void MapMaxLength(EditorHandler handler, IEditor editor) public static void MapMaxLength(EditorHandler handler, IEditor editor)
@@ -170,39 +140,22 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
public static void MapSelectionLength(EditorHandler handler, IEditor editor) public static void MapSelectionLength(EditorHandler handler, IEditor editor)
{ {
if (handler.PlatformView is null) return; // Selection would need to be added to SkiaEditor
handler.PlatformView.SelectionLength = editor.SelectionLength;
} }
public static void MapKeyboard(EditorHandler handler, IEditor editor) public static void MapKeyboard(EditorHandler handler, IEditor editor)
{ {
// Virtual keyboard type not applicable to desktop - stored for future use // Virtual keyboard type not applicable to desktop
} }
public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor) public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor)
{ {
if (handler.PlatformView is null) return; // Text alignment would require changes to SkiaEditor drawing
handler.PlatformView.HorizontalTextAlignment = editor.HorizontalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
} }
public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor) public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor)
{ {
if (handler.PlatformView is null) return; // Text alignment would require changes to SkiaEditor drawing
handler.PlatformView.VerticalTextAlignment = editor.VerticalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
} }
public static void MapBackground(EditorHandler handler, IEditor editor) public static void MapBackground(EditorHandler handler, IEditor editor)
@@ -211,7 +164,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (editor.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (editor.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.EditorBackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -219,9 +172,9 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (editor is Editor ve && ve.BackgroundColor != null) if (editor is VisualElement ve && ve.BackgroundColor != null)
{ {
handler.PlatformView.EditorBackgroundColor = ve.BackgroundColor; handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
} }

View File

@@ -0,0 +1,199 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for Entry control.
/// </summary>
public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
{
/// <summary>
/// Maps the property mapper for the handler.
/// </summary>
public static IPropertyMapper<IEntry, EntryHandler> Mapper = new PropertyMapper<IEntry, EntryHandler>(ViewHandler.ViewMapper)
{
[nameof(IEntry.Text)] = MapText,
[nameof(IEntry.TextColor)] = MapTextColor,
[nameof(IEntry.Placeholder)] = MapPlaceholder,
[nameof(IEntry.PlaceholderColor)] = MapPlaceholderColor,
[nameof(IEntry.Font)] = MapFont,
[nameof(IEntry.IsPassword)] = MapIsPassword,
[nameof(IEntry.MaxLength)] = MapMaxLength,
[nameof(IEntry.IsReadOnly)] = MapIsReadOnly,
[nameof(IEntry.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IEntry.CursorPosition)] = MapCursorPosition,
[nameof(IEntry.SelectionLength)] = MapSelectionLength,
[nameof(IEntry.ReturnType)] = MapReturnType,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IEntry.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
public EntryHandler() : base(Mapper, CommandMapper)
{
}
public EntryHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaEntry CreatePlatformView()
{
return new SkiaEntry();
}
protected override void ConnectHandler(SkiaEntry platformView)
{
base.ConnectHandler(platformView);
platformView.TextChanged += OnTextChanged;
platformView.Completed += OnCompleted;
}
protected override void DisconnectHandler(SkiaEntry platformView)
{
platformView.TextChanged -= OnTextChanged;
platformView.Completed -= OnCompleted;
base.DisconnectHandler(platformView);
}
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (VirtualView != null && VirtualView.Text != e.NewText)
{
VirtualView.Text = e.NewText;
}
}
private void OnCompleted(object? sender, EventArgs e)
{
VirtualView?.Completed();
}
public static void MapText(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView.Text != entry.Text)
{
handler.PlatformView.Text = entry.Text ?? "";
}
}
public static void MapTextColor(EntryHandler handler, IEntry entry)
{
if (entry.TextColor != null)
{
handler.PlatformView.TextColor = entry.TextColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapPlaceholder(EntryHandler handler, IEntry entry)
{
handler.PlatformView.Placeholder = entry.Placeholder ?? "";
handler.PlatformView.Invalidate();
}
public static void MapPlaceholderColor(EntryHandler handler, IEntry entry)
{
if (entry.PlaceholderColor != null)
{
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapFont(EntryHandler handler, IEntry entry)
{
var font = entry.Font;
if (font.Family != null)
{
handler.PlatformView.FontFamily = font.Family;
}
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.Invalidate();
}
public static void MapIsPassword(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsPassword = entry.IsPassword;
handler.PlatformView.Invalidate();
}
public static void MapMaxLength(EntryHandler handler, IEntry entry)
{
handler.PlatformView.MaxLength = entry.MaxLength;
}
public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsReadOnly = entry.IsReadOnly;
}
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
{
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
handler.PlatformView.Invalidate();
}
public static void MapCursorPosition(EntryHandler handler, IEntry entry)
{
handler.PlatformView.CursorPosition = entry.CursorPosition;
}
public static void MapSelectionLength(EntryHandler handler, IEntry entry)
{
// Selection length is handled internally by SkiaEntry
}
public static void MapReturnType(EntryHandler handler, IEntry entry)
{
// Return type affects keyboard on mobile; on desktop, Enter always completes
}
public static void MapIsEnabled(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsEnabled = entry.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(EntryHandler handler, IEntry entry)
{
var background = entry.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
}
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

@@ -3,7 +3,6 @@
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -29,13 +28,9 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
[nameof(IEntry.IsPassword)] = MapIsPassword, [nameof(IEntry.IsPassword)] = MapIsPassword,
[nameof(IEntry.ReturnType)] = MapReturnType, [nameof(IEntry.ReturnType)] = MapReturnType,
[nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility, [nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility,
[nameof(IEntry.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
[nameof(IEntry.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick,
}; };
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -70,24 +65,14 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private bool _isUpdatingText;
private void OnTextChanged(object? sender, Platform.TextChangedEventArgs e) private void OnTextChanged(object? sender, Platform.TextChangedEventArgs e)
{ {
if (VirtualView is null || PlatformView is null || _isUpdatingText) return; if (VirtualView is null || PlatformView is null) return;
if (VirtualView.Text != e.NewTextValue) if (VirtualView.Text != e.NewTextValue)
{
_isUpdatingText = true;
try
{ {
VirtualView.Text = e.NewTextValue ?? string.Empty; VirtualView.Text = e.NewTextValue ?? string.Empty;
} }
finally
{
_isUpdatingText = false;
}
}
} }
private void OnCompleted(object? sender, EventArgs e) private void OnCompleted(object? sender, EventArgs e)
@@ -97,7 +82,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
public static void MapText(EntryHandler handler, IEntry entry) public static void MapText(EntryHandler handler, IEntry entry)
{ {
if (handler.PlatformView is null || handler._isUpdatingText) return; if (handler.PlatformView is null) return;
if (handler.PlatformView.Text != entry.Text) if (handler.PlatformView.Text != entry.Text)
{ {
@@ -111,7 +96,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (entry.TextColor is not null) if (entry.TextColor is not null)
handler.PlatformView.TextColor = entry.TextColor; handler.PlatformView.TextColor = entry.TextColor.ToSKColor();
} }
public static void MapFont(EntryHandler handler, IEntry entry) public static void MapFont(EntryHandler handler, IEntry entry)
@@ -120,24 +105,19 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
var font = entry.Font; var font = entry.Font;
if (font.Size > 0) if (font.Size > 0)
handler.PlatformView.FontSize = font.Size; handler.PlatformView.FontSize = (float)font.Size;
if (!string.IsNullOrEmpty(font.Family)) if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family; handler.PlatformView.FontFamily = font.Family;
// Convert Font weight/slant to FontAttributes handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
FontAttributes attrs = FontAttributes.None; handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
} }
public static void MapCharacterSpacing(EntryHandler handler, IEntry entry) public static void MapCharacterSpacing(EntryHandler handler, IEntry entry)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = entry.CharacterSpacing; handler.PlatformView.CharacterSpacing = (float)entry.CharacterSpacing;
} }
public static void MapPlaceholder(EntryHandler handler, IEntry entry) public static void MapPlaceholder(EntryHandler handler, IEntry entry)
@@ -151,7 +131,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (entry.PlaceholderColor is not null) if (entry.PlaceholderColor is not null)
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor; handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor();
} }
public static void MapIsReadOnly(EntryHandler handler, IEntry entry) public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
@@ -197,28 +177,16 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
handler.PlatformView.ShowClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing; handler.PlatformView.ShowClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing;
} }
public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsTextPredictionEnabled = entry.IsTextPredictionEnabled;
}
public static void MapIsSpellCheckEnabled(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsSpellCheckEnabled = entry.IsSpellCheckEnabled;
}
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry) public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
{ {
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End, Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
_ => TextAlignment.Start _ => Platform.TextAlignment.Start
}; };
} }
@@ -228,10 +196,10 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch
{ {
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End, Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
_ => TextAlignment.Center _ => Platform.TextAlignment.Center
}; };
} }
@@ -241,29 +209,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView is null) return;
if (entry is Entry ve && ve.BackgroundColor != null)
{
handler.PlatformView.EntryBackgroundColor = ve.BackgroundColor;
// Also set base BackgroundColor so SkiaView.DrawBackground() respects transparency
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
}
}
public static void MapSelectAllOnDoubleClick(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView is null) return;
if (entry is BindableObject bindable)
{
handler.PlatformView.SelectAllOnDoubleClick = EntryExtensions.GetSelectAllOnDoubleClick(bindable);
} }
} }
} }

View File

@@ -1,105 +0,0 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Layouts;
namespace Microsoft.Maui.Platform.Linux.Handlers;
public class FlexLayoutHandler : LayoutHandler
{
public new static IPropertyMapper<FlexLayout, FlexLayoutHandler> Mapper = new PropertyMapper<FlexLayout, FlexLayoutHandler>(LayoutHandler.Mapper)
{
["Direction"] = MapDirection,
["Wrap"] = MapWrap,
["JustifyContent"] = MapJustifyContent,
["AlignItems"] = MapAlignItems,
["AlignContent"] = MapAlignContent
};
public FlexLayoutHandler() : base(Mapper)
{
}
protected override SkiaLayoutView CreatePlatformView()
{
return new SkiaFlexLayout();
}
public static void MapDirection(FlexLayoutHandler handler, FlexLayout layout)
{
if (handler.PlatformView is SkiaFlexLayout flexLayout)
{
flexLayout.Direction = layout.Direction switch
{
Microsoft.Maui.Layouts.FlexDirection.Row => FlexDirection.Row,
Microsoft.Maui.Layouts.FlexDirection.RowReverse => FlexDirection.RowReverse,
Microsoft.Maui.Layouts.FlexDirection.Column => FlexDirection.Column,
Microsoft.Maui.Layouts.FlexDirection.ColumnReverse => FlexDirection.ColumnReverse,
_ => FlexDirection.Row,
};
}
}
public static void MapWrap(FlexLayoutHandler handler, FlexLayout layout)
{
if (handler.PlatformView is SkiaFlexLayout flexLayout)
{
flexLayout.Wrap = layout.Wrap switch
{
Microsoft.Maui.Layouts.FlexWrap.NoWrap => FlexWrap.NoWrap,
Microsoft.Maui.Layouts.FlexWrap.Wrap => FlexWrap.Wrap,
Microsoft.Maui.Layouts.FlexWrap.Reverse => FlexWrap.WrapReverse,
_ => FlexWrap.NoWrap,
};
}
}
public static void MapJustifyContent(FlexLayoutHandler handler, FlexLayout layout)
{
if (handler.PlatformView is SkiaFlexLayout flexLayout)
{
flexLayout.JustifyContent = layout.JustifyContent switch
{
Microsoft.Maui.Layouts.FlexJustify.Start => FlexJustify.Start,
Microsoft.Maui.Layouts.FlexJustify.Center => FlexJustify.Center,
Microsoft.Maui.Layouts.FlexJustify.End => FlexJustify.End,
Microsoft.Maui.Layouts.FlexJustify.SpaceBetween => FlexJustify.SpaceBetween,
Microsoft.Maui.Layouts.FlexJustify.SpaceAround => FlexJustify.SpaceAround,
Microsoft.Maui.Layouts.FlexJustify.SpaceEvenly => FlexJustify.SpaceEvenly,
_ => FlexJustify.Start,
};
}
}
public static void MapAlignItems(FlexLayoutHandler handler, FlexLayout layout)
{
if (handler.PlatformView is SkiaFlexLayout flexLayout)
{
flexLayout.AlignItems = layout.AlignItems switch
{
Microsoft.Maui.Layouts.FlexAlignItems.Start => FlexAlignItems.Start,
Microsoft.Maui.Layouts.FlexAlignItems.Center => FlexAlignItems.Center,
Microsoft.Maui.Layouts.FlexAlignItems.End => FlexAlignItems.End,
Microsoft.Maui.Layouts.FlexAlignItems.Stretch => FlexAlignItems.Stretch,
_ => FlexAlignItems.Stretch,
};
}
}
public static void MapAlignContent(FlexLayoutHandler handler, FlexLayout layout)
{
if (handler.PlatformView is SkiaFlexLayout flexLayout)
{
flexLayout.AlignContent = layout.AlignContent switch
{
Microsoft.Maui.Layouts.FlexAlignContent.Start => FlexAlignContent.Start,
Microsoft.Maui.Layouts.FlexAlignContent.Center => FlexAlignContent.Center,
Microsoft.Maui.Layouts.FlexAlignContent.End => FlexAlignContent.End,
Microsoft.Maui.Layouts.FlexAlignContent.Stretch => FlexAlignContent.Stretch,
Microsoft.Maui.Layouts.FlexAlignContent.SpaceBetween => FlexAlignContent.SpaceBetween,
Microsoft.Maui.Layouts.FlexAlignContent.SpaceAround => FlexAlignContent.SpaceAround,
Microsoft.Maui.Layouts.FlexAlignContent.SpaceEvenly => FlexAlignContent.SpaceEvenly,
_ => FlexAlignContent.Stretch,
};
}
}
}

View File

@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -15,17 +13,12 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary> /// </summary>
public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage> public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage>
{ {
private bool _isUpdatingPresented;
public static IPropertyMapper<IFlyoutView, FlyoutPageHandler> Mapper = new PropertyMapper<IFlyoutView, FlyoutPageHandler>(ViewHandler.ViewMapper) public static IPropertyMapper<IFlyoutView, FlyoutPageHandler> Mapper = new PropertyMapper<IFlyoutView, FlyoutPageHandler>(ViewHandler.ViewMapper)
{ {
[nameof(IFlyoutView.Flyout)] = MapFlyout,
[nameof(IFlyoutView.Detail)] = MapDetail,
[nameof(IFlyoutView.IsPresented)] = MapIsPresented, [nameof(IFlyoutView.IsPresented)] = MapIsPresented,
[nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth, [nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth,
[nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled, [nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled,
[nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior, [nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior,
[nameof(IView.Background)] = MapBackground,
}; };
public static CommandMapper<IFlyoutView, FlyoutPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IFlyoutView, FlyoutPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -62,83 +55,14 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
private void OnIsPresentedChanged(object? sender, EventArgs e) private void OnIsPresentedChanged(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null || _isUpdatingPresented) return;
try
{
_isUpdatingPresented = true;
// Sync back to the virtual view // Sync back to the virtual view
if (VirtualView is FlyoutPage flyoutPage)
{
flyoutPage.IsPresented = PlatformView.IsPresented;
}
}
finally
{
_isUpdatingPresented = false;
}
}
public static void MapFlyout(FlyoutPageHandler handler, IFlyoutView flyoutView)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var flyout = flyoutView.Flyout;
if (flyout == null)
{
handler.PlatformView.Flyout = null;
return;
}
// Create handler for flyout content
if (flyout.Handler == null)
{
flyout.Handler = flyout.ToViewHandler(handler.MauiContext);
}
if (flyout.Handler?.PlatformView is SkiaView skiaFlyout)
{
handler.PlatformView.Flyout = skiaFlyout;
}
}
public static void MapDetail(FlyoutPageHandler handler, IFlyoutView flyoutView)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var detail = flyoutView.Detail;
if (detail == null)
{
handler.PlatformView.Detail = null;
return;
}
// Create handler for detail content
if (detail.Handler == null)
{
detail.Handler = detail.ToViewHandler(handler.MauiContext);
}
if (detail.Handler?.PlatformView is SkiaView skiaDetail)
{
handler.PlatformView.Detail = skiaDetail;
}
} }
public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView) public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView)
{ {
if (handler.PlatformView is null || handler._isUpdatingPresented) return; if (handler.PlatformView is null) return;
try
{
handler._isUpdatingPresented = true;
handler.PlatformView.IsPresented = flyoutView.IsPresented; handler.PlatformView.IsPresented = flyoutView.IsPresented;
} }
finally
{
handler._isUpdatingPresented = false;
}
}
public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView) public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView)
{ {
@@ -164,14 +88,4 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
_ => FlyoutLayoutBehavior.Default _ => FlyoutLayoutBehavior.Default
}; };
} }
public static void MapBackground(FlyoutPageHandler handler, IFlyoutView flyoutView)
{
if (handler.PlatformView is null) return;
if (flyoutView is FlyoutPage flyoutPage && flyoutPage.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.ScrimColor = solidBrush.Color.WithAlpha(100f / 255f);
}
}
} }

View File

@@ -3,7 +3,6 @@
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -38,36 +37,15 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
return new SkiaFrame(); return new SkiaFrame();
} }
protected override void ConnectHandler(SkiaFrame platformView)
{
base.ConnectHandler(platformView);
if (VirtualView is View view)
{
platformView.MauiView = view;
}
platformView.Tapped += OnPlatformViewTapped;
}
protected override void DisconnectHandler(SkiaFrame platformView)
{
platformView.Tapped -= OnPlatformViewTapped;
platformView.MauiView = null;
base.DisconnectHandler(platformView);
}
private void OnPlatformViewTapped(object? sender, EventArgs e)
{
if (VirtualView is View view)
{
GestureManager.ProcessTap(view, 0.0, 0.0);
}
}
public static void MapBorderColor(FrameHandler handler, Frame frame) public static void MapBorderColor(FrameHandler handler, Frame frame)
{ {
if (frame.BorderColor != null) if (frame.BorderColor != null)
{ {
handler.PlatformView.Stroke = frame.BorderColor; 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));
} }
} }
@@ -85,7 +63,11 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
{ {
if (frame.BackgroundColor != null) if (frame.BackgroundColor != null)
{ {
handler.PlatformView.BackgroundColor = frame.BackgroundColor; 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));
} }
} }
@@ -110,7 +92,7 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
// Create handler for content if it doesn't exist // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
content.Handler = content.ToViewHandler(handler.MauiContext); content.Handler = content.ToHandler(handler.MauiContext);
} }
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)

View File

@@ -1,856 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Manages gesture recognition and processing for MAUI views on Linux.
/// Handles tap, pan, swipe, pinch, and pointer gestures.
/// </summary>
public static class GestureManager
{
private const string Tag = "GestureManager";
private class GestureTrackingState
{
public double StartX { get; set; }
public double StartY { get; set; }
public double CurrentX { get; set; }
public double CurrentY { get; set; }
public DateTime StartTime { get; set; }
public bool IsPanning { get; set; }
public bool IsPressed { get; set; }
public bool IsPinching { get; set; }
public double PinchScale { get; set; } = 1.0;
}
private enum PointerEventType
{
Entered,
Exited,
Pressed,
Moved,
Released
}
// Cached reflection MethodInfo for internal MAUI methods
private static MethodInfo? _sendTappedMethod;
private static MethodInfo? _sendSwipedMethod;
private static MethodInfo? _sendPanMethod;
private static MethodInfo? _sendPinchMethod;
private static MethodInfo? _sendDragStartingMethod;
private static MethodInfo? _sendDragOverMethod;
private static MethodInfo? _sendDropMethod;
private static readonly Dictionary<PointerEventType, MethodInfo?> _pointerMethodCache = new();
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new();
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new();
/// <summary>
/// Minimum distance in pixels for a swipe gesture to be recognized.
/// </summary>
public static double SwipeMinDistance { get; set; } = 50.0;
/// <summary>
/// Maximum time in milliseconds for a swipe gesture to be recognized.
/// </summary>
public static double SwipeMaxTime { get; set; } = 500.0;
/// <summary>
/// Ratio threshold for determining swipe direction dominance.
/// </summary>
public static double SwipeDirectionThreshold { get; set; } = 0.5;
/// <summary>
/// Minimum distance in pixels before a pan gesture is recognized.
/// </summary>
public static double PanMinDistance { get; set; } = 10.0;
/// <summary>
/// Scale factor per scroll unit for pinch-via-scroll gestures.
/// </summary>
public static double PinchScrollScale { get; set; } = 0.1;
/// <summary>
/// Removes tracking entries for the specified view, preventing memory leaks
/// when views are disconnected from the visual tree.
/// </summary>
public static void CleanupView(View view)
{
if (view == null) return;
_tapTracking.Remove(view);
_gestureState.Remove(view);
}
/// <summary>
/// Processes a tap gesture on the specified view.
/// </summary>
public static bool ProcessTap(View? view, double x, double y)
{
if (view == null)
{
return false;
}
var current = view;
while (current != null)
{
var recognizers = current.GestureRecognizers;
if (recognizers != null && recognizers.Count > 0 && ProcessTapOnView(current, x, y))
{
return true;
}
var parent = current.Parent;
current = (parent is View parentView) ? parentView : null;
}
return false;
}
private static bool ProcessTapOnView(View view, double x, double y)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null || recognizers.Count == 0)
{
return false;
}
bool result = false;
foreach (var item in recognizers)
{
if (item is not TapGestureRecognizer tapRecognizer)
{
continue;
}
DiagnosticLog.Debug(Tag,
$"Processing TapGestureRecognizer on {view.GetType().Name}, CommandParameter={tapRecognizer.CommandParameter}, NumberOfTapsRequired={tapRecognizer.NumberOfTapsRequired}");
int numberOfTapsRequired = tapRecognizer.NumberOfTapsRequired;
if (numberOfTapsRequired > 1)
{
DateTime utcNow = DateTime.UtcNow;
if (!_tapTracking.TryGetValue(view, out var tracking))
{
_tapTracking[view] = (utcNow, 1);
DiagnosticLog.Debug(Tag, $"First tap 1/{numberOfTapsRequired}");
continue;
}
if (!((utcNow - tracking.lastTap).TotalMilliseconds < 300.0))
{
_tapTracking[view] = (utcNow, 1);
DiagnosticLog.Debug(Tag, $"Tap timeout, reset to 1/{numberOfTapsRequired}");
continue;
}
int tapCount = tracking.tapCount + 1;
if (tapCount < numberOfTapsRequired)
{
_tapTracking[view] = (utcNow, tapCount);
DiagnosticLog.Debug(Tag, $"Tap {tapCount}/{numberOfTapsRequired}, waiting for more taps");
continue;
}
_tapTracking.Remove(view);
}
// Try to raise the Tapped event via cached reflection
bool eventFired = false;
try
{
if (_sendTappedMethod == null)
{
_sendTappedMethod = typeof(TapGestureRecognizer).GetMethod(
"SendTapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendTappedMethod != null)
{
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
_sendTappedMethod.Invoke(tapRecognizer, new object[] { view, args });
DiagnosticLog.Debug(Tag, "SendTapped invoked successfully");
eventFired = true;
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendTapped failed", ex);
}
// Always invoke the Command if available (SendTapped may or may not invoke it internally)
if (!eventFired)
{
ICommand? command = tapRecognizer.Command;
if (command != null && command.CanExecute(tapRecognizer.CommandParameter))
{
DiagnosticLog.Debug(Tag, "Executing TapGestureRecognizer Command");
command.Execute(tapRecognizer.CommandParameter);
}
}
result = true;
}
return result;
}
/// <summary>
/// Checks if the view has any gesture recognizers.
/// </summary>
public static bool HasGestureRecognizers(View? view)
{
if (view == null)
{
return false;
}
return view.GestureRecognizers?.Count > 0;
}
/// <summary>
/// Checks if the view has a tap gesture recognizer.
/// </summary>
public static bool HasTapGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is TapGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Processes a pointer down event.
/// </summary>
public static void ProcessPointerDown(View? view, double x, double y)
{
if (view != null)
{
_gestureState[view] = new GestureTrackingState
{
StartX = x,
StartY = y,
CurrentX = x,
CurrentY = y,
StartTime = DateTime.UtcNow,
IsPanning = false,
IsPressed = true
};
ProcessPointerEvent(view, x, y, PointerEventType.Pressed);
}
}
/// <summary>
/// Processes a pointer move event.
/// </summary>
public static void ProcessPointerMove(View? view, double x, double y)
{
if (view == null)
{
return;
}
if (!_gestureState.TryGetValue(view, out var state))
{
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
return;
}
state.CurrentX = x;
state.CurrentY = y;
if (!state.IsPressed)
{
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
return;
}
double deltaX = x - state.StartX;
double deltaY = y - state.StartY;
if (Math.Sqrt(deltaX * deltaX + deltaY * deltaY) >= PanMinDistance)
{
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)(state.IsPanning ? 1 : 0));
state.IsPanning = true;
}
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
}
/// <summary>
/// Processes a pointer up event.
/// </summary>
public static void ProcessPointerUp(View? view, double x, double y)
{
if (view == null)
{
return;
}
if (_gestureState.TryGetValue(view, out var state))
{
state.CurrentX = x;
state.CurrentY = y;
double deltaX = x - state.StartX;
double deltaY = y - state.StartY;
double distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
double elapsed = (DateTime.UtcNow - state.StartTime).TotalMilliseconds;
if (distance >= SwipeMinDistance && elapsed <= SwipeMaxTime)
{
var direction = DetermineSwipeDirection(deltaX, deltaY);
if (direction != SwipeDirection.Right)
{
ProcessSwipeGesture(view, direction);
}
else if (Math.Abs(deltaX) > Math.Abs(deltaY) * SwipeDirectionThreshold)
{
ProcessSwipeGesture(view, (deltaX > 0.0) ? SwipeDirection.Right : SwipeDirection.Left);
}
}
if (state.IsPanning)
{
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)2);
}
else if (distance < 15.0 && elapsed < SwipeMaxTime)
{
DiagnosticLog.Debug(Tag, $"Detected tap on {view.GetType().Name} (distance={distance:F1}, elapsed={elapsed:F0}ms)");
ProcessTap(view, x, y);
}
_gestureState.Remove(view);
}
ProcessPointerEvent(view, x, y, PointerEventType.Released);
}
/// <summary>
/// Processes a pointer entered event.
/// </summary>
public static void ProcessPointerEntered(View? view, double x, double y)
{
if (view != null)
{
ProcessPointerEvent(view, x, y, PointerEventType.Entered);
}
}
/// <summary>
/// Processes a pointer exited event.
/// </summary>
public static void ProcessPointerExited(View? view, double x, double y)
{
if (view != null)
{
ProcessPointerEvent(view, x, y, PointerEventType.Exited);
}
}
private static SwipeDirection DetermineSwipeDirection(double deltaX, double deltaY)
{
double absX = Math.Abs(deltaX);
double absY = Math.Abs(deltaY);
if (absX > absY * SwipeDirectionThreshold)
{
if (deltaX > 0.0)
{
return SwipeDirection.Right;
}
return SwipeDirection.Left;
}
if (absY > absX * SwipeDirectionThreshold)
{
if (deltaY > 0.0)
{
return SwipeDirection.Down;
}
return SwipeDirection.Up;
}
if (deltaX > 0.0)
{
return SwipeDirection.Right;
}
return SwipeDirection.Left;
}
private static void ProcessSwipeGesture(View view, SwipeDirection direction)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
if (item is not SwipeGestureRecognizer swipeRecognizer || !swipeRecognizer.Direction.HasFlag(direction))
{
continue;
}
DiagnosticLog.Debug(Tag, $"Swipe detected: {direction}");
try
{
if (_sendSwipedMethod == null)
{
_sendSwipedMethod = typeof(SwipeGestureRecognizer).GetMethod(
"SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendSwipedMethod != null)
{
_sendSwipedMethod.Invoke(swipeRecognizer, new object[] { view, direction });
DiagnosticLog.Debug(Tag, "SendSwiped invoked successfully");
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendSwiped failed", ex);
}
ICommand? command = swipeRecognizer.Command;
if (command != null && command.CanExecute(swipeRecognizer.CommandParameter))
{
command.Execute(swipeRecognizer.CommandParameter);
}
}
}
private static void ProcessPanGesture(View view, double totalX, double totalY, GestureStatus status)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
if (item is not PanGestureRecognizer panRecognizer)
{
continue;
}
DiagnosticLog.Debug(Tag, $"Pan gesture: status={status}, totalX={totalX:F1}, totalY={totalY:F1}");
try
{
if (_sendPanMethod == null)
{
_sendPanMethod = typeof(PanGestureRecognizer).GetMethod(
"SendPan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendPanMethod != null)
{
_sendPanMethod.Invoke(panRecognizer, new object[]
{
view,
totalX,
totalY,
(int)status
});
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendPan failed", ex);
}
}
}
private static void ProcessPointerEvent(View view, double x, double y, PointerEventType eventType)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
if (item is not PointerGestureRecognizer pointerRecognizer)
{
continue;
}
try
{
string? methodName = eventType switch
{
PointerEventType.Entered => "SendPointerEntered",
PointerEventType.Exited => "SendPointerExited",
PointerEventType.Pressed => "SendPointerPressed",
PointerEventType.Moved => "SendPointerMoved",
PointerEventType.Released => "SendPointerReleased",
_ => null,
};
if (methodName == null)
{
continue;
}
if (!_pointerMethodCache.TryGetValue(eventType, out var method))
{
method = typeof(PointerGestureRecognizer).GetMethod(
methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
_pointerMethodCache[eventType] = method;
}
if (method != null)
{
var args = CreatePointerEventArgs(view, x, y);
method.Invoke(pointerRecognizer, new object[] { view, args });
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, $"Pointer event {eventType} failed", ex);
}
}
}
private static object CreatePointerEventArgs(View view, double x, double y)
{
try
{
var type = typeof(PointerGestureRecognizer).Assembly.GetType("Microsoft.Maui.Controls.PointerEventArgs");
if (type != null)
{
var ctor = type.GetConstructors().FirstOrDefault();
if (ctor != null)
{
return ctor.Invoke(new object[0]);
}
}
}
catch (Exception ex)
{
DiagnosticLog.Debug("GestureManager", "PointerEventArgs creation failed", ex);
}
return null!;
}
/// <summary>
/// Processes a scroll event that may be a pinch gesture (Ctrl+Scroll).
/// Returns true if the scroll was consumed as a pinch gesture.
/// </summary>
public static bool ProcessScrollAsPinch(View? view, double x, double y, double deltaY, bool isCtrlPressed)
{
if (view == null || !isCtrlPressed)
{
return false;
}
// Check if view has a pinch gesture recognizer
if (!HasPinchGestureRecognizer(view))
{
return false;
}
// Get or create gesture state
if (!_gestureState.TryGetValue(view, out var state))
{
state = new GestureTrackingState
{
StartX = x,
StartY = y,
CurrentX = x,
CurrentY = y,
StartTime = DateTime.UtcNow,
PinchScale = 1.0
};
_gestureState[view] = state;
}
// Calculate new scale based on scroll delta
double scaleDelta = 1.0 + (deltaY * PinchScrollScale);
state.PinchScale *= scaleDelta;
// Clamp scale to reasonable bounds
state.PinchScale = Math.Clamp(state.PinchScale, 0.1, 10.0);
GestureStatus status;
if (!state.IsPinching)
{
state.IsPinching = true;
status = GestureStatus.Started;
}
else
{
status = GestureStatus.Running;
}
ProcessPinchGesture(view, state.PinchScale, x, y, status);
return true;
}
/// <summary>
/// Ends an ongoing pinch gesture.
/// </summary>
public static void EndPinchGesture(View? view)
{
if (view == null) return;
if (_gestureState.TryGetValue(view, out var state) && state.IsPinching)
{
ProcessPinchGesture(view, state.PinchScale, state.CurrentX, state.CurrentY, GestureStatus.Completed);
state.IsPinching = false;
state.PinchScale = 1.0;
}
}
private static void ProcessPinchGesture(View view, double scale, double originX, double originY, GestureStatus status)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
if (item is not PinchGestureRecognizer pinchRecognizer)
{
continue;
}
DiagnosticLog.Debug(Tag, $"Pinch gesture: status={status}, scale={scale:F2}, origin=({originX:F0},{originY:F0})");
try
{
if (_sendPinchMethod == null)
{
_sendPinchMethod = typeof(PinchGestureRecognizer).GetMethod(
"SendPinch", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendPinchMethod != null)
{
var scaleOrigin = new Point(originX / view.Width, originY / view.Height);
_sendPinchMethod.Invoke(pinchRecognizer, new object[]
{
view,
scale,
scaleOrigin,
status
});
DiagnosticLog.Debug(Tag, "SendPinch invoked successfully");
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendPinch failed", ex);
}
}
}
/// <summary>
/// Checks if the view has a pinch gesture recognizer.
/// </summary>
public static bool HasPinchGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PinchGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a swipe gesture recognizer.
/// </summary>
public static bool HasSwipeGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is SwipeGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a pan gesture recognizer.
/// </summary>
public static bool HasPanGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PanGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a pointer gesture recognizer.
/// </summary>
public static bool HasPointerGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PointerGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a drag gesture recognizer.
/// </summary>
public static bool HasDragGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is DragGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a drop gesture recognizer.
/// </summary>
public static bool HasDropGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is DropGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Initiates a drag operation from the specified view.
/// </summary>
public static void StartDrag(View? view, double x, double y)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
if (item is not DragGestureRecognizer dragRecognizer) continue;
DiagnosticLog.Debug(Tag, $"Starting drag from {view.GetType().Name}");
try
{
if (_sendDragStartingMethod == null)
{
_sendDragStartingMethod = typeof(DragGestureRecognizer).GetMethod(
"SendDragStarting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendDragStartingMethod != null)
{
_sendDragStartingMethod.Invoke(dragRecognizer, new object[] { view });
DiagnosticLog.Debug(Tag, "SendDragStarting invoked successfully");
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendDragStarting failed", ex);
}
}
}
/// <summary>
/// Processes a drag enter event on the specified view.
/// </summary>
public static void ProcessDragEnter(View? view, double x, double y, object? data)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
if (item is not DropGestureRecognizer dropRecognizer) continue;
DiagnosticLog.Debug(Tag, $"Drag enter on {view.GetType().Name}");
try
{
if (_sendDragOverMethod == null)
{
_sendDragOverMethod = typeof(DropGestureRecognizer).GetMethod(
"SendDragOver", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendDragOverMethod != null)
{
_sendDragOverMethod.Invoke(dropRecognizer, new object[] { view });
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendDragOver failed", ex);
}
}
}
/// <summary>
/// Processes a drop event on the specified view.
/// </summary>
public static void ProcessDrop(View? view, double x, double y, object? data)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
if (item is not DropGestureRecognizer dropRecognizer) continue;
DiagnosticLog.Debug(Tag, $"Drop on {view.GetType().Name}");
try
{
if (_sendDropMethod == null)
{
_sendDropMethod = typeof(DropGestureRecognizer).GetMethod(
"SendDrop", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendDropMethod != null)
{
_sendDropMethod.Invoke(dropRecognizer, new object[] { view });
}
}
catch (Exception ex)
{
DiagnosticLog.Error(Tag, "SendDrop failed", ex);
}
}
}
}

View File

@@ -51,7 +51,7 @@ public partial class GraphicsViewHandler : ViewHandler<IGraphicsView, SkiaGraphi
if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }

View File

@@ -1,265 +0,0 @@
// 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;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for WebView using native GTK WebKitGTK widget.
/// </summary>
public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
{
private GtkWebViewPlatformView? _platformWebView;
private bool _isRegisteredWithHost;
private SKRect _lastBounds;
public static IPropertyMapper<IWebView, GtkWebViewHandler> Mapper = new PropertyMapper<IWebView, GtkWebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IWebView.Source)] = MapSource,
};
public static CommandMapper<IWebView, GtkWebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
[nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload,
};
public GtkWebViewHandler() : base(Mapper, CommandMapper)
{
}
public GtkWebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override GtkWebViewProxy CreatePlatformView()
{
_platformWebView = new GtkWebViewPlatformView();
return new GtkWebViewProxy(this, _platformWebView);
}
protected override void ConnectHandler(GtkWebViewProxy platformView)
{
base.ConnectHandler(platformView);
if (_platformWebView != null)
{
_platformWebView.NavigationStarted += OnNavigationStarted;
_platformWebView.NavigationCompleted += OnNavigationCompleted;
_platformWebView.ScriptDialogRequested += OnScriptDialogRequested;
}
DiagnosticLog.Debug("GtkWebViewHandler", "ConnectHandler - WebView ready");
}
protected override void DisconnectHandler(GtkWebViewProxy platformView)
{
if (_platformWebView != null)
{
_platformWebView.NavigationStarted -= OnNavigationStarted;
_platformWebView.NavigationCompleted -= OnNavigationCompleted;
_platformWebView.ScriptDialogRequested -= OnScriptDialogRequested;
UnregisterFromHost();
_platformWebView.Dispose();
_platformWebView = null;
}
base.DisconnectHandler(platformView);
}
private async void OnScriptDialogRequested(object? sender,
(ScriptDialogType Type, string Message, Action<bool> Callback) e)
{
DiagnosticLog.Debug("GtkWebViewHandler", $"Script dialog requested: type={e.Type}, message={e.Message}");
string title = e.Type switch
{
ScriptDialogType.Alert => "Alert",
ScriptDialogType.Confirm => "Confirm",
ScriptDialogType.Prompt => "Prompt",
_ => "Message"
};
string? acceptButton = e.Type == ScriptDialogType.Alert ? "OK" : "OK";
string? cancelButton = e.Type == ScriptDialogType.Alert ? null : "Cancel";
try
{
bool result = await LinuxDialogService.ShowAlertAsync(title, e.Message, acceptButton, cancelButton);
e.Callback(result);
DiagnosticLog.Debug("GtkWebViewHandler", $"Dialog result: {result}");
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewHandler", $"Error showing dialog: {ex.Message}", ex);
e.Callback(false);
}
}
private void OnNavigationStarted(object? sender, string uri)
{
DiagnosticLog.Debug("GtkWebViewHandler", $"Navigation started: {uri}");
try
{
GLibNative.IdleAdd(() =>
{
try
{
if (VirtualView is IWebViewController controller)
{
var args = new Microsoft.Maui.Controls.WebNavigatingEventArgs(
WebNavigationEvent.NewPage, null, uri);
controller.SendNavigating(args);
DiagnosticLog.Debug("GtkWebViewHandler", "Sent Navigating event to VirtualView");
}
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewHandler", $"Error in SendNavigating: {ex.Message}", ex);
}
return false;
});
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewHandler", $"Error dispatching navigation started: {ex.Message}", ex);
}
}
private void OnNavigationCompleted(object? sender, (string Url, bool Success) e)
{
DiagnosticLog.Debug("GtkWebViewHandler", $"Navigation completed: {e.Url} (Success: {e.Success})");
try
{
GLibNative.IdleAdd(() =>
{
try
{
if (VirtualView is IWebViewController controller)
{
var result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
var args = new Microsoft.Maui.Controls.WebNavigatedEventArgs(
WebNavigationEvent.NewPage, null, e.Url, result);
controller.SendNavigated(args);
bool canGoBack = _platformWebView?.CanGoBack() ?? false;
bool canGoForward = _platformWebView?.CanGoForward() ?? false;
controller.CanGoBack = canGoBack;
controller.CanGoForward = canGoForward;
DiagnosticLog.Debug("GtkWebViewHandler", $"Sent Navigated, CanGoBack={canGoBack}, CanGoForward={canGoForward}");
}
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewHandler", $"Error in SendNavigated: {ex.Message}", ex);
}
return false;
});
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewHandler", $"Error dispatching navigation completed: {ex.Message}", ex);
}
}
internal void RegisterWithHost(SKRect bounds)
{
if (_platformWebView == null)
return;
var hostService = GtkHostService.Instance;
if (hostService.HostWindow == null || hostService.WebViewManager == null)
{
DiagnosticLog.Warn("GtkWebViewHandler", "GTK host not initialized, cannot register WebView");
return;
}
int x = (int)bounds.Left;
int y = (int)bounds.Top;
int width = (int)bounds.Width;
int height = (int)bounds.Height;
if (width <= 0 || height <= 0)
{
DiagnosticLog.Warn("GtkWebViewHandler", $"Skipping invalid bounds: {bounds}");
return;
}
if (!_isRegisteredWithHost)
{
hostService.HostWindow.AddWebView(_platformWebView.Widget, x, y, width, height);
_isRegisteredWithHost = true;
DiagnosticLog.Debug("GtkWebViewHandler", $"Registered WebView at ({x}, {y}) size {width}x{height}");
}
else if (bounds != _lastBounds)
{
hostService.HostWindow.MoveResizeWebView(_platformWebView.Widget, x, y, width, height);
DiagnosticLog.Debug("GtkWebViewHandler", $"Updated WebView to ({x}, {y}) size {width}x{height}");
}
_lastBounds = bounds;
}
private void UnregisterFromHost()
{
if (_isRegisteredWithHost && _platformWebView != null)
{
var hostService = GtkHostService.Instance;
if (hostService.HostWindow != null)
{
hostService.HostWindow.RemoveWebView(_platformWebView.Widget);
DiagnosticLog.Debug("GtkWebViewHandler", "Unregistered WebView from host");
}
_isRegisteredWithHost = false;
}
}
public static void MapSource(GtkWebViewHandler handler, IWebView webView)
{
if (handler._platformWebView == null)
return;
var source = webView.Source;
DiagnosticLog.Debug("GtkWebViewHandler", $"MapSource: {source?.GetType().Name ?? "null"}");
if (source is UrlWebViewSource urlSource)
{
var url = urlSource.Url;
if (!string.IsNullOrEmpty(url))
{
handler._platformWebView.Navigate(url);
}
}
else if (source is HtmlWebViewSource htmlSource)
{
var html = htmlSource.Html;
if (!string.IsNullOrEmpty(html))
{
handler._platformWebView.LoadHtml(html, htmlSource.BaseUrl);
}
}
}
public static void MapGoBack(GtkWebViewHandler handler, IWebView webView, object? args)
{
DiagnosticLog.Debug("GtkWebViewHandler", $"MapGoBack called, CanGoBack={handler._platformWebView?.CanGoBack()}");
handler._platformWebView?.GoBack();
}
public static void MapGoForward(GtkWebViewHandler handler, IWebView webView, object? args)
{
DiagnosticLog.Debug("GtkWebViewHandler", $"MapGoForward called, CanGoForward={handler._platformWebView?.CanGoForward()}");
handler._platformWebView?.GoForward();
}
public static void MapReload(GtkWebViewHandler handler, IWebView webView, object? args)
{
DiagnosticLog.Debug("GtkWebViewHandler", "MapReload called");
handler._platformWebView?.Reload();
}
}

View File

@@ -1,60 +0,0 @@
using System.Collections.Generic;
using Microsoft.Maui.Platform.Linux.Window;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Manages WebView instances within the GTK host window.
/// Handles creation, layout updates, and cleanup of WebKit-based web views.
/// </summary>
public sealed class GtkWebViewManager
{
private readonly GtkHostWindow _host;
private readonly Dictionary<object, GtkWebViewPlatformView> _webViews = new();
public GtkWebViewManager(GtkHostWindow host)
{
_host = host;
}
public GtkWebViewPlatformView CreateWebView(object key, int x, int y, int width, int height)
{
var webView = new GtkWebViewPlatformView();
_webViews[key] = webView;
_host.AddWebView(webView.Widget, x, y, width, height);
return webView;
}
public void UpdateLayout(object key, int x, int y, int width, int height)
{
if (_webViews.TryGetValue(key, out var webView))
{
_host.MoveResizeWebView(webView.Widget, x, y, width, height);
}
}
public GtkWebViewPlatformView? GetWebView(object key)
{
return _webViews.TryGetValue(key, out var webView) ? webView : null;
}
public void RemoveWebView(object key)
{
if (_webViews.TryGetValue(key, out var webView))
{
_host.RemoveWebView(webView.Widget);
webView.Dispose();
_webViews.Remove(key);
}
}
public void Clear()
{
foreach (var kvp in _webViews)
{
_host.RemoveWebView(kvp.Value.Widget);
kvp.Value.Dispose();
}
_webViews.Clear();
}
}

View File

@@ -1,545 +0,0 @@
using System;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Type of JavaScript dialog.
/// </summary>
public enum ScriptDialogType
{
Alert = 0,
Confirm = 1,
Prompt = 2,
BeforeUnloadConfirm = 3
}
/// <summary>
/// GTK-based WebView platform view using WebKitGTK.
/// Provides web browsing capabilities within MAUI applications.
/// </summary>
public sealed class GtkWebViewPlatformView : IDisposable
{
private IntPtr _widget;
private bool _disposed;
private string? _currentUri;
private ulong _loadChangedSignalId;
private ulong _scriptDialogSignalId;
private WebKitNative.LoadChangedCallback? _loadChangedCallback;
private WebKitNative.ScriptDialogCallback? _scriptDialogCallback;
private EventHandler<Microsoft.Maui.Controls.AppThemeChangedEventArgs>? _themeChangedHandler;
public IntPtr Widget => _widget;
public string? CurrentUri => _currentUri;
public event EventHandler<string>? NavigationStarted;
public event EventHandler<(string Url, bool Success)>? NavigationCompleted;
public event EventHandler<string>? TitleChanged;
public event EventHandler<(ScriptDialogType Type, string Message, Action<bool> Callback)>? ScriptDialogRequested;
public GtkWebViewPlatformView()
{
if (!WebKitNative.Initialize())
{
throw new InvalidOperationException("Failed to initialize WebKitGTK. Is libwebkit2gtk-4.x installed?");
}
_widget = WebKitNative.WebViewNew();
if (_widget == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create WebKitWebView widget");
}
WebKitNative.ConfigureSettings(_widget);
_loadChangedCallback = OnLoadChanged;
_loadChangedSignalId = WebKitNative.ConnectLoadChanged(_widget, _loadChangedCallback);
// Connect to script-dialog signal to intercept JavaScript alerts/confirms/prompts
_scriptDialogCallback = OnScriptDialog;
_scriptDialogSignalId = WebKitNative.ConnectScriptDialog(_widget, _scriptDialogCallback);
// Set initial background color based on theme
UpdateBackgroundForTheme();
// Subscribe to theme changes to update background color
_themeChangedHandler = (sender, args) =>
{
GLibNative.IdleAdd(() =>
{
UpdateBackgroundForTheme();
return false;
});
};
if (Microsoft.Maui.Controls.Application.Current != null)
{
Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged += _themeChangedHandler;
}
DiagnosticLog.Debug("GtkWebViewPlatformView", "Created WebKitWebView widget");
}
/// <summary>
/// Updates the WebView background color based on the current app theme.
/// </summary>
public void UpdateBackgroundForTheme()
{
if (_widget == IntPtr.Zero) return;
var isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
if (isDark)
{
// Dark theme: use a dark gray background
WebKitNative.SetBackgroundColor(_widget, 0.12, 0.12, 0.12, 1.0); // #1E1E1E
}
else
{
// Light theme: use white background
WebKitNative.SetBackgroundColor(_widget, 1.0, 1.0, 1.0, 1.0);
}
}
private bool OnScriptDialog(IntPtr webView, IntPtr dialog, IntPtr userData)
{
try
{
var webkitDialogType = WebKitNative.GetScriptDialogType(dialog);
var dialogType = (ScriptDialogType)(int)webkitDialogType;
var message = WebKitNative.GetScriptDialogMessage(dialog) ?? "";
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Script dialog: type={dialogType}, message={message}");
// Get the parent window for proper modal behavior
IntPtr parentWindow = GtkHostService.Instance.HostWindow?.Window ?? IntPtr.Zero;
// Handle prompt dialogs specially - they need a text entry
if (dialogType == ScriptDialogType.Prompt)
{
return HandlePromptDialog(dialog, message, parentWindow);
}
// Determine dialog type and buttons based on JavaScript dialog type
int messageType = GtkNative.GTK_MESSAGE_INFO;
int buttons = GtkNative.GTK_BUTTONS_OK;
switch (dialogType)
{
case ScriptDialogType.Alert:
messageType = GtkNative.GTK_MESSAGE_INFO;
buttons = GtkNative.GTK_BUTTONS_OK;
break;
case ScriptDialogType.Confirm:
case ScriptDialogType.BeforeUnloadConfirm:
messageType = GtkNative.GTK_MESSAGE_QUESTION;
buttons = GtkNative.GTK_BUTTONS_OK_CANCEL;
break;
}
// Create and show native GTK message dialog
IntPtr gtkDialog = GtkNative.gtk_message_dialog_new(
parentWindow,
GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT,
messageType,
buttons,
message,
IntPtr.Zero);
if (gtkDialog != IntPtr.Zero)
{
// Set dialog title based on type
string title = dialogType switch
{
ScriptDialogType.Alert => "Alert",
ScriptDialogType.Confirm => "Confirm",
ScriptDialogType.BeforeUnloadConfirm => "Leave Page?",
_ => "Message"
};
GtkNative.gtk_window_set_title(gtkDialog, title);
// Apply theme-aware CSS styling based on app's current theme
ApplyDialogTheme(gtkDialog);
// Make dialog modal to parent if we have a parent
if (parentWindow != IntPtr.Zero)
{
GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow);
GtkNative.gtk_window_set_modal(gtkDialog, true);
}
// Run the dialog synchronously - this blocks until user responds
int response = GtkNative.gtk_dialog_run(gtkDialog);
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Dialog response: {response}");
// Set the confirmed state for confirm dialogs
if (dialogType == ScriptDialogType.Confirm || dialogType == ScriptDialogType.BeforeUnloadConfirm)
{
bool confirmed = response == GtkNative.GTK_RESPONSE_OK || response == GtkNative.GTK_RESPONSE_YES;
WebKitNative.SetScriptDialogConfirmed(dialog, confirmed);
}
// Clean up
GtkNative.gtk_widget_destroy(gtkDialog);
}
// Return true to indicate we handled the dialog (prevents WebKitGTK's default)
return true;
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error in OnScriptDialog: {ex.Message}", ex);
// Return false on error to let WebKitGTK try its default handling
return false;
}
}
private bool HandlePromptDialog(IntPtr webkitDialog, string message, IntPtr parentWindow)
{
try
{
// Get the default text for the prompt
string? defaultText = WebKitNative.GetScriptDialogPromptDefaultText(webkitDialog) ?? "";
// Create a custom dialog with OK/Cancel buttons
IntPtr gtkDialog = GtkNative.gtk_dialog_new_with_buttons(
"Prompt",
parentWindow,
GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT,
"_Cancel",
GtkNative.GTK_RESPONSE_CANCEL,
"_OK",
GtkNative.GTK_RESPONSE_OK,
IntPtr.Zero);
if (gtkDialog == IntPtr.Zero)
{
DiagnosticLog.Error("GtkWebViewPlatformView", "Failed to create prompt dialog");
return false;
}
// Apply theme-aware CSS styling
ApplyDialogTheme(gtkDialog);
// Get the content area
IntPtr contentArea = GtkNative.gtk_dialog_get_content_area(gtkDialog);
// Create a vertical box for the content
IntPtr vbox = GtkNative.gtk_box_new(GtkNative.GTK_ORIENTATION_VERTICAL, 10);
GtkNative.gtk_widget_set_margin_start(vbox, 12);
GtkNative.gtk_widget_set_margin_end(vbox, 12);
GtkNative.gtk_widget_set_margin_top(vbox, 12);
GtkNative.gtk_widget_set_margin_bottom(vbox, 12);
// Add the message label
IntPtr label = GtkNative.gtk_label_new(message);
GtkNative.gtk_box_pack_start(vbox, label, false, false, 0);
// Add the text entry
IntPtr entry = GtkNative.gtk_entry_new();
GtkNative.gtk_entry_set_text(entry, defaultText);
GtkNative.gtk_box_pack_start(vbox, entry, false, false, 0);
// Add the vbox to content area
GtkNative.gtk_box_pack_start(contentArea, vbox, true, true, 0);
// Make dialog modal
if (parentWindow != IntPtr.Zero)
{
GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow);
GtkNative.gtk_window_set_modal(gtkDialog, true);
}
// Show all widgets
GtkNative.gtk_widget_show_all(gtkDialog);
// Run the dialog
int response = GtkNative.gtk_dialog_run(gtkDialog);
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Prompt dialog response: {response}");
if (response == GtkNative.GTK_RESPONSE_OK)
{
// Get the text from the entry
IntPtr textPtr = GtkNative.gtk_entry_get_text(entry);
string? enteredText = textPtr != IntPtr.Zero
? System.Runtime.InteropServices.Marshal.PtrToStringUTF8(textPtr)
: "";
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Prompt text: {enteredText}");
// Set the prompt response
WebKitNative.SetScriptDialogPromptText(webkitDialog, enteredText ?? "");
}
else
{
// User cancelled - for prompts, not confirming means returning null
// WebKit handles this by not calling prompt_set_text
}
// Clean up
GtkNative.gtk_widget_destroy(gtkDialog);
return true;
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error in HandlePromptDialog: {ex.Message}", ex);
return false;
}
}
/// <summary>
/// Applies theme-aware CSS styling to a GTK dialog based on the app's current theme.
/// </summary>
private void ApplyDialogTheme(IntPtr gtkDialog)
{
try
{
// Check the app's current theme (not the system theme)
bool isDark = Microsoft.Maui.Controls.Application.Current?.UserAppTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
// If UserAppTheme is Unspecified, fall back to RequestedTheme
if (Microsoft.Maui.Controls.Application.Current?.UserAppTheme == Microsoft.Maui.ApplicationModel.AppTheme.Unspecified)
{
isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
}
DiagnosticLog.Debug("GtkWebViewPlatformView", $"ApplyDialogTheme: isDark={isDark}, UserAppTheme={Microsoft.Maui.Controls.Application.Current?.UserAppTheme}");
// Create comprehensive CSS based on the theme - targeting all dialog elements
string css = isDark
? @"
* {
background-color: #303030;
color: #E0E0E0;
}
window, dialog, messagedialog, .background {
background-color: #303030;
color: #E0E0E0;
}
headerbar, headerbar *, .titlebar, .titlebar * {
background-color: #252525;
background-image: none;
color: #E0E0E0;
border-color: #404040;
box-shadow: none;
}
headerbar button, .titlebar button {
background-color: #353535;
background-image: none;
color: #E0E0E0;
}
.dialog-action-area, .dialog-action-box, actionbar {
background-color: #303030;
}
label, .message-dialog-message, .message-dialog-secondary-message {
color: #E0E0E0;
}
button {
background-image: none;
background-color: #505050;
color: #E0E0E0;
border-color: #606060;
}
button:hover {
background-color: #606060;
}
entry {
background-color: #404040;
color: #E0E0E0;
}
"
: @"
* {
background-color: #FFFFFF;
color: #212121;
}
window, dialog, messagedialog, .background {
background-color: #FFFFFF;
color: #212121;
}
headerbar, headerbar *, .titlebar, .titlebar * {
background-color: #F5F5F5;
background-image: none;
color: #212121;
border-color: #E0E0E0;
box-shadow: none;
}
headerbar button, .titlebar button {
background-color: #EBEBEB;
background-image: none;
color: #212121;
}
.dialog-action-area, .dialog-action-box, actionbar {
background-color: #FFFFFF;
}
label, .message-dialog-message, .message-dialog-secondary-message {
color: #212121;
}
button {
background-image: none;
background-color: #F5F5F5;
color: #212121;
border-color: #E0E0E0;
}
button:hover {
background-color: #E0E0E0;
}
entry {
background-color: #FFFFFF;
color: #212121;
}
";
// Create CSS provider and apply to the screen so all child widgets inherit it
IntPtr cssProvider = GtkNative.gtk_css_provider_new();
if (cssProvider != IntPtr.Zero)
{
GtkNative.gtk_css_provider_load_from_data(cssProvider, css, -1, IntPtr.Zero);
// Get the screen from the dialog and apply CSS to entire screen for this dialog
IntPtr screen = GtkNative.gtk_widget_get_screen(gtkDialog);
if (screen == IntPtr.Zero)
{
screen = GtkNative.gdk_screen_get_default();
}
if (screen != IntPtr.Zero)
{
GtkNative.gtk_style_context_add_provider_for_screen(screen, cssProvider, GtkNative.GTK_STYLE_PROVIDER_PRIORITY_USER);
}
}
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error applying dialog theme: {ex.Message}", ex);
}
}
private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData)
{
try
{
string uri = WebKitNative.GetUri(webView) ?? _currentUri ?? "";
switch ((WebKitNative.WebKitLoadEvent)loadEvent)
{
case WebKitNative.WebKitLoadEvent.Started:
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load started: " + uri);
NavigationStarted?.Invoke(this, uri);
break;
case WebKitNative.WebKitLoadEvent.Finished:
_currentUri = uri;
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load finished: " + uri);
NavigationCompleted?.Invoke(this, (uri, true));
break;
case WebKitNative.WebKitLoadEvent.Committed:
_currentUri = uri;
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load committed: " + uri);
break;
case WebKitNative.WebKitLoadEvent.Redirected:
break;
}
}
catch (Exception ex)
{
DiagnosticLog.Error("GtkWebViewPlatformView", "Error in OnLoadChanged: " + ex.Message, ex);
}
}
public void Navigate(string uri)
{
if (_widget != IntPtr.Zero)
{
WebKitNative.LoadUri(_widget, uri);
DiagnosticLog.Debug("GtkWebViewPlatformView", "Navigate to: " + uri);
}
}
public void LoadHtml(string html, string? baseUri = null)
{
if (_widget != IntPtr.Zero)
{
WebKitNative.LoadHtml(_widget, html, baseUri);
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load HTML content");
}
}
public void GoBack()
{
if (_widget != IntPtr.Zero)
{
WebKitNative.GoBack(_widget);
}
}
public void GoForward()
{
if (_widget != IntPtr.Zero)
{
WebKitNative.GoForward(_widget);
}
}
public bool CanGoBack()
{
return _widget != IntPtr.Zero && WebKitNative.CanGoBack(_widget);
}
public bool CanGoForward()
{
return _widget != IntPtr.Zero && WebKitNative.CanGoForward(_widget);
}
public void Reload()
{
if (_widget != IntPtr.Zero)
{
WebKitNative.Reload(_widget);
}
}
public void Stop()
{
if (_widget != IntPtr.Zero)
{
WebKitNative.StopLoading(_widget);
}
}
public string? GetTitle()
{
return _widget == IntPtr.Zero ? null : WebKitNative.GetTitle(_widget);
}
public string? GetUri()
{
return _widget == IntPtr.Zero ? null : WebKitNative.GetUri(_widget);
}
public void SetJavascriptEnabled(bool enabled)
{
if (_widget != IntPtr.Zero)
{
WebKitNative.SetJavascriptEnabled(_widget, enabled);
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
// Unsubscribe from theme changes
if (_themeChangedHandler != null && Microsoft.Maui.Controls.Application.Current != null)
{
Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged -= _themeChangedHandler;
_themeChangedHandler = null;
}
if (_widget != IntPtr.Zero)
{
WebKitNative.DisconnectLoadChanged(_widget);
WebKitNative.DisconnectScriptDialog(_widget);
}
_widget = IntPtr.Zero;
_loadChangedCallback = null;
_scriptDialogCallback = null;
}
}
}

View File

@@ -1,71 +0,0 @@
// 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;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Proxy view that bridges SkiaView layout to GTK WebView positioning.
/// </summary>
public class GtkWebViewProxy : SkiaView
{
private readonly GtkWebViewHandler _handler;
private readonly GtkWebViewPlatformView _platformView;
public GtkWebViewPlatformView PlatformView => _platformView;
public bool CanGoBack => _platformView.CanGoBack();
public bool CanGoForward => _platformView.CanGoForward();
public GtkWebViewProxy(GtkWebViewHandler handler, GtkWebViewPlatformView platformView)
{
_handler = handler;
_platformView = platformView;
}
public override void Arrange(Rect bounds)
{
base.Arrange(bounds);
// Bounds are already in absolute window coordinates - use them directly
// The Skia layout system uses absolute coordinates throughout
_handler.RegisterWithHost(new SKRect((float)Bounds.Left, (float)Bounds.Top, (float)Bounds.Right, (float)Bounds.Bottom));
}
public override void Draw(SKCanvas canvas)
{
// Draw transparent placeholder - actual WebView is rendered by GTK
using var paint = new SKPaint
{
Color = new SKColor(0, 0, 0, 0),
Style = SKPaintStyle.Fill
};
canvas.DrawRect(new SKRect((float)Bounds.Left, (float)Bounds.Top, (float)Bounds.Right, (float)Bounds.Bottom), paint);
}
public void Navigate(string url)
{
_platformView.Navigate(url);
}
public void LoadHtml(string html, string? baseUrl = null)
{
_platformView.LoadHtml(html, baseUrl);
}
public void GoBack()
{
_platformView.GoBack();
}
public void GoForward()
{
_platformView.GoForward();
}
public void Reload()
{
_platformView.Reload();
}
}

View File

@@ -24,11 +24,6 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius, [nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
[nameof(IPadding.Padding)] = MapPadding, [nameof(IPadding.Padding)] = MapPadding,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
[nameof(IView.Width)] = MapWidth,
[nameof(IView.Height)] = MapHeight,
["VerticalOptions"] = MapVerticalOptions,
["HorizontalOptions"] = MapHorizontalOptions,
}; };
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -123,13 +118,13 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (imageButton.StrokeColor is not null) if (imageButton.StrokeColor is not null)
handler.PlatformView.StrokeColor = imageButton.StrokeColor; handler.PlatformView.StrokeColor = imageButton.StrokeColor.ToSKColor();
} }
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton) public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.StrokeThickness = imageButton.StrokeThickness; handler.PlatformView.StrokeThickness = (float)imageButton.StrokeThickness;
} }
public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton) public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton)
@@ -141,7 +136,12 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton) public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.Padding = imageButton.Padding;
var padding = imageButton.Padding;
handler.PlatformView.PaddingLeft = (float)padding.Left;
handler.PlatformView.PaddingTop = (float)padding.Top;
handler.PlatformView.PaddingRight = (float)padding.Right;
handler.PlatformView.PaddingBottom = (float)padding.Bottom;
} }
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton) public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
@@ -150,59 +150,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.ImageBackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapBackgroundColor(ImageButtonHandler handler, IImageButton imageButton)
{
if (handler.PlatformView is null) return;
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn && imgBtn.BackgroundColor is not null)
{
handler.PlatformView.ImageBackgroundColor = imgBtn.BackgroundColor;
}
}
public static void MapWidth(ImageButtonHandler handler, IImageButton imageButton)
{
if (handler.PlatformView is null) return;
// Map WidthRequest from the MAUI ImageButton to the platform view
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn && imgBtn.WidthRequest > 0)
{
handler.PlatformView.WidthRequest = imgBtn.WidthRequest;
}
}
public static void MapHeight(ImageButtonHandler handler, IImageButton imageButton)
{
if (handler.PlatformView is null) return;
// Map HeightRequest from the MAUI ImageButton to the platform view
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn && imgBtn.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = imgBtn.HeightRequest;
}
}
public static void MapVerticalOptions(ImageButtonHandler handler, IImageButton imageButton)
{
if (handler.PlatformView is null) return;
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn)
{
handler.PlatformView.VerticalOptions = imgBtn.VerticalOptions;
}
}
public static void MapHorizontalOptions(ImageButtonHandler handler, IImageButton imageButton)
{
if (handler.PlatformView is null) return;
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn)
{
handler.PlatformView.HorizontalOptions = imgBtn.HorizontalOptions;
} }
} }

View File

@@ -1,11 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -23,10 +20,6 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
[nameof(IImage.IsOpaque)] = MapIsOpaque, [nameof(IImage.IsOpaque)] = MapIsOpaque,
[nameof(IImageSourcePart.Source)] = MapSource, [nameof(IImageSourcePart.Source)] = MapSource,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["Width"] = MapWidth,
["Height"] = MapHeight,
["HorizontalOptions"] = MapHorizontalOptions,
["VerticalOptions"] = MapVerticalOptions,
}; };
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -95,19 +88,6 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// Extract width/height requests from Image control
if (image is Image img)
{
if (img.WidthRequest > 0)
{
handler.PlatformView.WidthRequest = img.WidthRequest;
}
if (img.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = img.HeightRequest;
}
}
handler.SourceLoader.UpdateImageSourceAsync(); handler.SourceLoader.UpdateImageSourceAsync();
} }
@@ -117,57 +97,7 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.ImageBackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapWidth(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img && img.WidthRequest > 0)
{
handler.PlatformView.WidthRequest = img.WidthRequest;
DiagnosticLog.Debug("ImageHandler", $"MapWidth: {img.WidthRequest}");
}
else if (image.Width > 0)
{
handler.PlatformView.WidthRequest = image.Width;
}
}
public static void MapHeight(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img && img.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = img.HeightRequest;
DiagnosticLog.Debug("ImageHandler", $"MapHeight: {img.HeightRequest}");
}
else if (image.Height > 0)
{
handler.PlatformView.HeightRequest = image.Height;
}
}
public static void MapHorizontalOptions(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img)
{
handler.PlatformView.HorizontalOptions = img.HorizontalOptions;
}
}
public static void MapVerticalOptions(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img)
{
handler.PlatformView.VerticalOptions = img.VerticalOptions;
} }
} }
@@ -232,14 +162,6 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
await _handler.PlatformView!.LoadFromStreamAsync(stream); await _handler.PlatformView!.LoadFromStreamAsync(stream);
} }
} }
else if (source is FontImageSource fontSource)
{
var bitmap = RenderFontImageSource(fontSource, _handler.PlatformView!.WidthRequest, _handler.PlatformView.HeightRequest);
if (bitmap != null)
{
_handler.PlatformView.LoadFromBitmap(bitmap);
}
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -254,73 +176,5 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
} }
} }
} }
private static SKBitmap? RenderFontImageSource(FontImageSource fontSource, double requestedWidth, double requestedHeight)
{
string glyph = fontSource.Glyph;
if (string.IsNullOrEmpty(glyph))
{
return null;
}
int size = (int)Math.Max(requestedWidth > 0 ? requestedWidth : 24.0, requestedHeight > 0 ? requestedHeight : 24.0);
size = Math.Max(size, 16);
SKColor color = fontSource.Color?.ToSKColor() ?? SKColors.Black;
SKBitmap bitmap = new SKBitmap(size, size, false);
using SKCanvas canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
SKTypeface? typeface = null;
if (!string.IsNullOrEmpty(fontSource.FontFamily))
{
string[] fontPaths = new string[]
{
"/usr/share/fonts/truetype/" + fontSource.FontFamily + ".ttf",
"/usr/share/fonts/opentype/" + fontSource.FontFamily + ".otf",
"/usr/local/share/fonts/" + fontSource.FontFamily + ".ttf",
Path.Combine(AppContext.BaseDirectory, fontSource.FontFamily + ".ttf")
};
foreach (string path in fontPaths)
{
if (File.Exists(path))
{
typeface = SKTypeface.FromFile(path, 0);
if (typeface != null)
{
break;
}
}
}
if (typeface == null)
{
typeface = SKTypeface.FromFamilyName(fontSource.FontFamily);
}
}
if (typeface == null)
{
typeface = SKTypeface.Default;
}
float fontSize = size * 0.8f;
using SKFont font = new SKFont(typeface, fontSize, 1f, 0f);
using SKPaint paint = new SKPaint(font)
{
Color = color,
IsAntialias = true,
TextAlign = SKTextAlign.Center
};
SKRect bounds = default;
paint.MeasureText(glyph, ref bounds);
float x = size / 2f;
float y = (size - bounds.Top - bounds.Bottom) / 2f;
canvas.DrawText(glyph, x, y, paint);
return bitmap;
}
} }
} }

View File

@@ -1,157 +0,0 @@
// 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.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for IndicatorView on Linux using Skia rendering.
/// Maps IndicatorView to SkiaIndicatorView platform view.
/// </summary>
public partial class IndicatorViewHandler : ViewHandler<IndicatorView, SkiaIndicatorView>
{
private bool _isUpdatingPosition;
public static IPropertyMapper<IndicatorView, IndicatorViewHandler> Mapper =
new PropertyMapper<IndicatorView, IndicatorViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IndicatorView.Count)] = MapCount,
[nameof(IndicatorView.Position)] = MapPosition,
[nameof(IndicatorView.IndicatorColor)] = MapIndicatorColor,
[nameof(IndicatorView.SelectedIndicatorColor)] = MapSelectedIndicatorColor,
[nameof(IndicatorView.IndicatorSize)] = MapIndicatorSize,
[nameof(IndicatorView.IndicatorsShape)] = MapIndicatorsShape,
[nameof(IndicatorView.MaximumVisible)] = MapMaximumVisible,
[nameof(IndicatorView.HideSingle)] = MapHideSingle,
[nameof(IndicatorView.ItemsSource)] = MapItemsSource,
};
public static CommandMapper<IndicatorView, IndicatorViewHandler> CommandMapper =
new(ViewHandler.ViewCommandMapper)
{
};
public IndicatorViewHandler() : base(Mapper, CommandMapper)
{
}
public IndicatorViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaIndicatorView CreatePlatformView()
{
return new SkiaIndicatorView();
}
protected override void ConnectHandler(SkiaIndicatorView platformView)
{
base.ConnectHandler(platformView);
// SkiaIndicatorView doesn't have position changed event, but we can add one if needed
}
protected override void DisconnectHandler(SkiaIndicatorView platformView)
{
base.DisconnectHandler(platformView);
}
public static void MapCount(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Count = indicatorView.Count;
}
public static void MapPosition(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null || handler._isUpdatingPosition) return;
try
{
handler._isUpdatingPosition = true;
handler.PlatformView.Position = indicatorView.Position;
}
finally
{
handler._isUpdatingPosition = false;
}
}
public static void MapIndicatorColor(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
if (indicatorView.IndicatorColor is not null)
{
handler.PlatformView.IndicatorColor = indicatorView.IndicatorColor;
}
}
public static void MapSelectedIndicatorColor(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
if (indicatorView.SelectedIndicatorColor is not null)
{
handler.PlatformView.SelectedIndicatorColor = indicatorView.SelectedIndicatorColor;
}
}
public static void MapIndicatorSize(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IndicatorSize = (float)indicatorView.IndicatorSize;
handler.PlatformView.SelectedIndicatorSize = (float)indicatorView.IndicatorSize;
}
public static void MapIndicatorsShape(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IndicatorShape = indicatorView.IndicatorsShape switch
{
Controls.IndicatorShape.Circle => Platform.IndicatorShape.Circle,
Controls.IndicatorShape.Square => Platform.IndicatorShape.Square,
_ => Platform.IndicatorShape.Circle
};
}
public static void MapMaximumVisible(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.MaximumVisible = indicatorView.MaximumVisible;
}
public static void MapHideSingle(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.HideSingle = indicatorView.HideSingle;
}
public static void MapItemsSource(IndicatorViewHandler handler, IndicatorView indicatorView)
{
if (handler.PlatformView is null) return;
// Count items from ItemsSource
int count = 0;
if (indicatorView.ItemsSource is System.Collections.ICollection collection)
{
count = collection.Count;
}
else if (indicatorView.ItemsSource is System.Collections.IEnumerable enumerable)
{
foreach (var _ in enumerable)
{
count++;
}
}
handler.PlatformView.Count = count;
}
}

View File

@@ -143,7 +143,7 @@ public partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, Skia
if (itemsView.Background is SolidColorBrush solidBrush) if (itemsView.Background is SolidColorBrush solidBrush)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
} }
} }

View File

@@ -0,0 +1,174 @@
// 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 SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for Label control.
/// </summary>
public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
{
/// <summary>
/// Maps the property mapper for the handler.
/// </summary>
public static IPropertyMapper<ILabel, LabelHandler> Mapper = new PropertyMapper<ILabel, LabelHandler>(ViewHandler.ViewMapper)
{
[nameof(ILabel.Text)] = MapText,
[nameof(ILabel.TextColor)] = MapTextColor,
[nameof(ILabel.Font)] = MapFont,
[nameof(ILabel.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(ILabel.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(ILabel.LineBreakMode)] = MapLineBreakMode,
[nameof(ILabel.MaxLines)] = MapMaxLines,
[nameof(ILabel.Padding)] = MapPadding,
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
[nameof(ILabel.LineHeight)] = MapLineHeight,
[nameof(ILabel.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
public LabelHandler() : base(Mapper, CommandMapper)
{
}
public LabelHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaLabel CreatePlatformView()
{
return new SkiaLabel();
}
public static void MapText(LabelHandler handler, ILabel label)
{
handler.PlatformView.Text = label.Text ?? "";
handler.PlatformView.Invalidate();
}
public static void MapTextColor(LabelHandler handler, ILabel label)
{
if (label.TextColor != null)
{
handler.PlatformView.TextColor = label.TextColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapFont(LabelHandler handler, ILabel label)
{
var font = label.Font;
if (font.Family != null)
{
handler.PlatformView.FontFamily = font.Family;
}
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.IsBold = font.Weight == FontWeight.Bold;
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic;
handler.PlatformView.Invalidate();
}
public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label)
{
handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
handler.PlatformView.Invalidate();
}
public static void MapVerticalTextAlignment(LabelHandler handler, ILabel label)
{
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Center
};
handler.PlatformView.Invalidate();
}
public static void MapLineBreakMode(LabelHandler handler, ILabel label)
{
handler.PlatformView.LineBreakMode = label.LineBreakMode switch
{
Microsoft.Maui.LineBreakMode.NoWrap => LineBreakMode.NoWrap,
Microsoft.Maui.LineBreakMode.WordWrap => LineBreakMode.WordWrap,
Microsoft.Maui.LineBreakMode.CharacterWrap => LineBreakMode.CharacterWrap,
Microsoft.Maui.LineBreakMode.HeadTruncation => LineBreakMode.HeadTruncation,
Microsoft.Maui.LineBreakMode.TailTruncation => LineBreakMode.TailTruncation,
Microsoft.Maui.LineBreakMode.MiddleTruncation => LineBreakMode.MiddleTruncation,
_ => LineBreakMode.TailTruncation
};
handler.PlatformView.Invalidate();
}
public static void MapMaxLines(LabelHandler handler, ILabel label)
{
handler.PlatformView.MaxLines = label.MaxLines;
handler.PlatformView.Invalidate();
}
public static void MapPadding(LabelHandler handler, ILabel label)
{
var padding = label.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.Invalidate();
}
public static void MapTextDecorations(LabelHandler handler, ILabel label)
{
var decorations = label.TextDecorations;
handler.PlatformView.IsUnderline = decorations.HasFlag(TextDecorations.Underline);
handler.PlatformView.IsStrikethrough = decorations.HasFlag(TextDecorations.Strikethrough);
handler.PlatformView.Invalidate();
}
public static void MapLineHeight(LabelHandler handler, ILabel label)
{
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

@@ -1,11 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.Linq;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Window;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -32,7 +29,6 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
["FormattedText"] = MapFormattedText,
}; };
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -53,45 +49,6 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
return new SkiaLabel(); return new SkiaLabel();
} }
protected override void ConnectHandler(SkiaLabel platformView)
{
base.ConnectHandler(platformView);
if (VirtualView is View view)
{
platformView.MauiView = view;
// Set hand cursor if the label has tap gesture recognizers
if (view.GestureRecognizers.OfType<TapGestureRecognizer>().Any())
{
platformView.CursorType = CursorType.Hand;
}
}
// Explicitly map LineBreakMode on connect - MAUI may not trigger property change for defaults
if (VirtualView is Microsoft.Maui.Controls.Label mauiLabel)
{
platformView.LineBreakMode = mauiLabel.LineBreakMode;
}
platformView.Tapped += OnPlatformViewTapped;
}
protected override void DisconnectHandler(SkiaLabel platformView)
{
platformView.Tapped -= OnPlatformViewTapped;
platformView.MauiView = null;
base.DisconnectHandler(platformView);
}
private void OnPlatformViewTapped(object? sender, EventArgs e)
{
if (VirtualView is View view)
{
GestureManager.ProcessTap(view, 0, 0);
}
}
public static void MapText(LabelHandler handler, ILabel label) public static void MapText(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
@@ -103,7 +60,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (label.TextColor is not null) if (label.TextColor is not null)
handler.PlatformView.TextColor = label.TextColor; handler.PlatformView.TextColor = label.TextColor.ToSKColor();
} }
public static void MapFont(LabelHandler handler, ILabel label) public static void MapFont(LabelHandler handler, ILabel label)
@@ -112,37 +69,32 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
var font = label.Font; var font = label.Font;
if (font.Size > 0) if (font.Size > 0)
handler.PlatformView.FontSize = font.Size; handler.PlatformView.FontSize = (float)font.Size;
if (!string.IsNullOrEmpty(font.Family)) if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family; handler.PlatformView.FontFamily = font.Family;
// Convert Font weight/slant to FontAttributes handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
FontAttributes attrs = FontAttributes.None; handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
} }
public static void MapCharacterSpacing(LabelHandler handler, ILabel label) public static void MapCharacterSpacing(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = label.CharacterSpacing; handler.PlatformView.CharacterSpacing = (float)label.CharacterSpacing;
} }
public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label) public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// Map MAUI TextAlignment to our TextAlignment // Map MAUI TextAlignment to our internal TextAlignment
handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch
{ {
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End, Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
_ => TextAlignment.Start _ => Platform.TextAlignment.Start
}; };
} }
@@ -152,23 +104,25 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
{ {
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End, Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
_ => TextAlignment.Center _ => Platform.TextAlignment.Center
}; };
} }
public static void MapTextDecorations(LabelHandler handler, ILabel label) public static void MapTextDecorations(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.TextDecorations = label.TextDecorations;
handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0;
handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0;
} }
public static void MapLineHeight(LabelHandler handler, ILabel label) public static void MapLineHeight(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
handler.PlatformView.LineHeight = label.LineHeight; handler.PlatformView.LineHeight = (float)label.LineHeight;
} }
public static void MapLineBreakMode(LabelHandler handler, ILabel label) public static void MapLineBreakMode(LabelHandler handler, ILabel label)
@@ -178,7 +132,16 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
// LineBreakMode is on Label control, not ILabel interface // LineBreakMode is on Label control, not ILabel interface
if (label is Microsoft.Maui.Controls.Label mauiLabel) if (label is Microsoft.Maui.Controls.Label mauiLabel)
{ {
handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode; 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
};
} }
} }
@@ -198,11 +161,11 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
var padding = label.Padding; var padding = label.Padding;
handler.PlatformView.Padding = new Thickness( handler.PlatformView.Padding = new SKRect(
padding.Left, (float)padding.Left,
padding.Top, (float)padding.Top,
padding.Right, (float)padding.Right,
padding.Bottom); (float)padding.Bottom);
} }
public static void MapBackground(LabelHandler handler, ILabel label) public static void MapBackground(LabelHandler handler, ILabel label)
@@ -211,7 +174,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
if (label.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (label.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -242,17 +205,4 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
_ => LayoutOptions.Start _ => LayoutOptions.Start
}; };
} }
public static void MapFormattedText(LabelHandler handler, ILabel label)
{
if (handler.PlatformView is null) return;
if (label is not Label mauiLabel)
{
handler.PlatformView.FormattedText = null;
return;
}
handler.PlatformView.FormattedText = mauiLabel.FormattedText;
}
} }

View File

@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -65,7 +63,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// (e.g., in ItemTemplates for CollectionView) // (e.g., in ItemTemplates for CollectionView)
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{ {
platformView.BackgroundColor = ve.BackgroundColor; platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
platformView.Invalidate(); platformView.Invalidate();
} }
@@ -80,7 +78,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Create handler for child if it doesn't exist // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToViewHandler(MauiContext); child.Handler = child.ToHandler(MauiContext);
} }
if (child.Handler?.PlatformView is SkiaView skiaChild) if (child.Handler?.PlatformView is SkiaView skiaChild)
@@ -100,7 +98,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
var background = layout.Background; var background = layout.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null) if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
} }
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
@@ -109,7 +107,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
{ {
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{ {
handler.PlatformView.BackgroundColor = ve.BackgroundColor; handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
} }
@@ -280,13 +278,13 @@ public partial class GridHandler : LayoutHandler
protected override void ConnectHandler(SkiaLayoutView platformView) protected override void ConnectHandler(SkiaLayoutView platformView)
{ {
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}"); Console.WriteLine($"[GridHandler.ConnectHandler] Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
// Map definitions on connect // Map definitions on connect
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null) if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
{ {
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}"); Console.WriteLine($"[GridHandler.ConnectHandler] Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
UpdateRowDefinitions(grid, gridLayout); UpdateRowDefinitions(grid, gridLayout);
UpdateColumnDefinitions(grid, gridLayout); UpdateColumnDefinitions(grid, gridLayout);
@@ -296,13 +294,13 @@ public partial class GridHandler : LayoutHandler
var child = gridLayout[i]; var child = gridLayout[i];
if (child == null) continue; if (child == null) continue;
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}"); Console.WriteLine($"[GridHandler.ConnectHandler] Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
// Create handler for child if it doesn't exist // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToViewHandler(MauiContext); child.Handler = child.ToHandler(MauiContext);
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}"); Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
} }
if (child.Handler?.PlatformView is SkiaView skiaChild) if (child.Handler?.PlatformView is SkiaView skiaChild)
@@ -316,11 +314,11 @@ public partial class GridHandler : LayoutHandler
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView); rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
} }
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Adding child[{i}] at row={row}, col={column}"); Console.WriteLine($"[GridHandler.ConnectHandler] Adding child[{i}] at row={row}, col={column}");
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan); grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
} }
} }
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid now has {grid.Children.Count} SkiaView children"); Console.WriteLine($"[GridHandler.ConnectHandler] Grid now has {grid.Children.Count} SkiaView children");
} }
} }

View File

@@ -3,8 +3,6 @@
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -55,7 +53,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Explicitly map BackgroundColor since it may be set before handler creation // Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{ {
platformView.BackgroundColor = ve.BackgroundColor; platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
} }
for (int i = 0; i < VirtualView.Count; i++) for (int i = 0; i < VirtualView.Count; i++)
@@ -66,7 +64,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Create handler for child if it doesn't exist // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToViewHandler(MauiContext); child.Handler = child.ToHandler(MauiContext);
} }
// Add child's platform view to our layout // Add child's platform view to our layout
@@ -89,7 +87,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
@@ -144,7 +142,12 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
if (layout is IPadding paddable) if (layout is IPadding paddable)
{ {
handler.PlatformView.Padding = paddable.Padding; 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.InvalidateMeasure();
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
@@ -246,20 +249,24 @@ public partial class GridHandler : LayoutHandler
// Don't call base - we handle children specially for Grid // Don't call base - we handle children specially for Grid
if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return; if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return;
DiagnosticLog.Debug("GridHandler", $"ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols"); 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 // Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{ {
platformView.BackgroundColor = ve.BackgroundColor; platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
} }
// Explicitly map Padding since it may be set before handler creation // Explicitly map Padding since it may be set before handler creation
if (VirtualView is IPadding paddable) if (VirtualView is IPadding paddable)
{ {
var padding = paddable.Padding; var padding = paddable.Padding;
platformView.Padding = padding; platformView.Padding = new SKRect(
DiagnosticLog.Debug("GridHandler", $"Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}"); (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 // Map row/column definitions first
@@ -272,12 +279,12 @@ public partial class GridHandler : LayoutHandler
var child = gridLayout[i]; var child = gridLayout[i];
if (child == null) continue; if (child == null) continue;
DiagnosticLog.Debug("GridHandler", $"Processing child {i}: {child.GetType().Name}"); Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}");
// Create handler for child if it doesn't exist // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToViewHandler(MauiContext); child.Handler = child.ToHandler(MauiContext);
} }
// Get grid position from attached properties // Get grid position from attached properties
@@ -290,20 +297,21 @@ public partial class GridHandler : LayoutHandler
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
} }
DiagnosticLog.Debug("GridHandler", $"Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}"); Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
// Add child's platform view to our grid // Add child's platform view to our grid
if (child.Handler?.PlatformView is SkiaView skiaChild) if (child.Handler?.PlatformView is SkiaView skiaChild)
{ {
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan); grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
DiagnosticLog.Debug("GridHandler", $"Added child {i} to grid"); Console.WriteLine($"[GridHandler] Added child {i} to grid");
} }
} }
DiagnosticLog.Debug("GridHandler", "ConnectHandler complete"); Console.WriteLine($"[GridHandler] ConnectHandler complete");
} }
catch (Exception ex) catch (Exception ex)
{ {
DiagnosticLog.Error("GridHandler", $"EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}", ex); Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}");
throw; throw;
} }
} }

View File

@@ -1,376 +0,0 @@
// 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 Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for MenuBar on Linux using Skia rendering.
/// Maps MenuBar to SkiaMenuBar platform view.
/// </summary>
public partial class MenuBarHandler : ElementHandler<IMenuBar, SkiaMenuBar>
{
public static IPropertyMapper<IMenuBar, MenuBarHandler> Mapper =
new PropertyMapper<IMenuBar, MenuBarHandler>()
{
[nameof(IMenuBar.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<IMenuBar, MenuBarHandler> CommandMapper =
new()
{
["Add"] = MapAdd,
["Remove"] = MapRemove,
["Clear"] = MapClear,
["Insert"] = MapInsert,
};
public MenuBarHandler() : base(Mapper, CommandMapper)
{
}
public MenuBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaMenuBar CreatePlatformElement()
{
return new SkiaMenuBar();
}
protected override void ConnectHandler(SkiaMenuBar platformView)
{
base.ConnectHandler(platformView);
SyncMenuBarItems();
}
protected override void DisconnectHandler(SkiaMenuBar platformView)
{
platformView.Items.Clear();
base.DisconnectHandler(platformView);
}
private void SyncMenuBarItems()
{
if (PlatformView is null || VirtualView is null) return;
PlatformView.Items.Clear();
foreach (var menuBarItem in VirtualView)
{
if (menuBarItem is MenuBarItem mauiItem)
{
var platformItem = CreatePlatformMenuBarItem(mauiItem);
PlatformView.Items.Add(platformItem);
}
}
PlatformView.Invalidate();
}
private static Platform.MenuBarItem CreatePlatformMenuBarItem(MenuBarItem mauiItem)
{
var platformItem = new Platform.MenuBarItem
{
Text = mauiItem.Text ?? ""
};
// MenuBarItem inherits from BaseMenuItem which has a collection
// Use cast to IEnumerable to iterate
if (mauiItem is System.Collections.IEnumerable enumerable)
{
foreach (var child in enumerable)
{
if (child is MenuFlyoutItem flyoutItem)
{
var menuItem = CreatePlatformMenuItem(flyoutItem);
platformItem.Items.Add(menuItem);
}
else if (child is MenuFlyoutSubItem subItem)
{
var menuItem = CreatePlatformMenuItemWithSubs(subItem);
platformItem.Items.Add(menuItem);
}
else if (child is MenuFlyoutSeparator)
{
platformItem.Items.Add(new Platform.MenuItem { IsSeparator = true });
}
}
}
return platformItem;
}
private static Platform.MenuItem CreatePlatformMenuItem(MenuFlyoutItem mauiItem)
{
var menuItem = new Platform.MenuItem
{
Text = mauiItem.Text ?? "",
IsEnabled = mauiItem.IsEnabled,
IconSource = mauiItem.IconImageSource?.ToString()
};
// Map keyboard accelerator
if (mauiItem.KeyboardAccelerators.Count > 0)
{
var accel = mauiItem.KeyboardAccelerators[0];
menuItem.Shortcut = FormatKeyboardAccelerator(accel);
}
// Connect click event
menuItem.Clicked += (s, e) =>
{
if (mauiItem.Command?.CanExecute(mauiItem.CommandParameter) == true)
{
mauiItem.Command.Execute(mauiItem.CommandParameter);
}
(mauiItem as IMenuFlyoutItem)?.Clicked();
};
return menuItem;
}
private static Platform.MenuItem CreatePlatformMenuItemWithSubs(MenuFlyoutSubItem mauiSubItem)
{
var menuItem = new Platform.MenuItem
{
Text = mauiSubItem.Text ?? "",
IsEnabled = mauiSubItem.IsEnabled,
IconSource = mauiSubItem.IconImageSource?.ToString()
};
// MenuFlyoutSubItem is enumerable
if (mauiSubItem is System.Collections.IEnumerable enumerable)
{
foreach (var child in enumerable)
{
if (child is MenuFlyoutItem flyoutItem)
{
menuItem.SubItems.Add(CreatePlatformMenuItem(flyoutItem));
}
else if (child is MenuFlyoutSubItem nestedSubItem)
{
menuItem.SubItems.Add(CreatePlatformMenuItemWithSubs(nestedSubItem));
}
else if (child is MenuFlyoutSeparator)
{
menuItem.SubItems.Add(new Platform.MenuItem { IsSeparator = true });
}
}
}
return menuItem;
}
private static string FormatKeyboardAccelerator(KeyboardAccelerator accel)
{
var parts = new List<string>();
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Ctrl))
parts.Add("Ctrl");
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Alt))
parts.Add("Alt");
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Shift))
parts.Add("Shift");
parts.Add(accel.Key ?? "");
return string.Join("+", parts);
}
public static void MapIsEnabled(MenuBarHandler handler, IMenuBar menuBar)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = menuBar.IsEnabled;
}
public static void MapAdd(MenuBarHandler handler, IMenuBar menuBar, object? args)
{
handler.SyncMenuBarItems();
}
public static void MapRemove(MenuBarHandler handler, IMenuBar menuBar, object? args)
{
handler.SyncMenuBarItems();
}
public static void MapClear(MenuBarHandler handler, IMenuBar menuBar, object? args)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Items.Clear();
handler.PlatformView.Invalidate();
}
public static void MapInsert(MenuBarHandler handler, IMenuBar menuBar, object? args)
{
handler.SyncMenuBarItems();
}
}
/// <summary>
/// Handler for MenuFlyout (context menu) on Linux using Skia rendering.
/// Maps IMenuFlyout to SkiaMenuFlyout platform view.
/// </summary>
public partial class MenuFlyoutHandler : ElementHandler<IMenuFlyout, SkiaMenuFlyout>
{
public static IPropertyMapper<IMenuFlyout, MenuFlyoutHandler> Mapper =
new PropertyMapper<IMenuFlyout, MenuFlyoutHandler>()
{
};
public static CommandMapper<IMenuFlyout, MenuFlyoutHandler> CommandMapper =
new()
{
["Add"] = MapAdd,
["Remove"] = MapRemove,
["Clear"] = MapClear,
};
public MenuFlyoutHandler() : base(Mapper, CommandMapper)
{
}
public MenuFlyoutHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaMenuFlyout CreatePlatformElement()
{
return new SkiaMenuFlyout();
}
protected override void ConnectHandler(SkiaMenuFlyout platformView)
{
base.ConnectHandler(platformView);
SyncMenuItems();
}
protected override void DisconnectHandler(SkiaMenuFlyout platformView)
{
platformView.Items.Clear();
base.DisconnectHandler(platformView);
}
private void SyncMenuItems()
{
if (PlatformView is null || VirtualView is null) return;
PlatformView.Items.Clear();
foreach (var item in VirtualView)
{
if (item is MenuFlyoutItem flyoutItem)
{
PlatformView.Items.Add(CreatePlatformMenuItem(flyoutItem));
}
else if (item is MenuFlyoutSubItem subItem)
{
PlatformView.Items.Add(CreatePlatformMenuItemWithSubs(subItem));
}
else if (item is MenuFlyoutSeparator)
{
PlatformView.Items.Add(new Platform.MenuItem { IsSeparator = true });
}
}
}
private static Platform.MenuItem CreatePlatformMenuItem(MenuFlyoutItem mauiItem)
{
var menuItem = new Platform.MenuItem
{
Text = mauiItem.Text ?? "",
IsEnabled = mauiItem.IsEnabled,
IconSource = mauiItem.IconImageSource?.ToString()
};
// Map keyboard accelerator
if (mauiItem.KeyboardAccelerators.Count > 0)
{
var accel = mauiItem.KeyboardAccelerators[0];
menuItem.Shortcut = FormatKeyboardAccelerator(accel);
}
// Connect click event
menuItem.Clicked += (s, e) =>
{
if (mauiItem.Command?.CanExecute(mauiItem.CommandParameter) == true)
{
mauiItem.Command.Execute(mauiItem.CommandParameter);
}
(mauiItem as IMenuFlyoutItem)?.Clicked();
};
return menuItem;
}
private static Platform.MenuItem CreatePlatformMenuItemWithSubs(MenuFlyoutSubItem mauiSubItem)
{
var menuItem = new Platform.MenuItem
{
Text = mauiSubItem.Text ?? "",
IsEnabled = mauiSubItem.IsEnabled,
IconSource = mauiSubItem.IconImageSource?.ToString()
};
// MenuFlyoutSubItem is enumerable
if (mauiSubItem is System.Collections.IEnumerable enumerable)
{
foreach (var child in enumerable)
{
if (child is MenuFlyoutItem flyoutItem)
{
menuItem.SubItems.Add(CreatePlatformMenuItem(flyoutItem));
}
else if (child is MenuFlyoutSubItem nestedSubItem)
{
menuItem.SubItems.Add(CreatePlatformMenuItemWithSubs(nestedSubItem));
}
else if (child is MenuFlyoutSeparator)
{
menuItem.SubItems.Add(new Platform.MenuItem { IsSeparator = true });
}
}
}
return menuItem;
}
private static string FormatKeyboardAccelerator(KeyboardAccelerator accel)
{
var parts = new List<string>();
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Ctrl))
parts.Add("Ctrl");
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Alt))
parts.Add("Alt");
if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Shift))
parts.Add("Shift");
parts.Add(accel.Key ?? "");
return string.Join("+", parts);
}
public static void MapAdd(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args)
{
handler.SyncMenuItems();
}
public static void MapRemove(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args)
{
handler.SyncMenuItems();
}
public static void MapClear(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Items.Clear();
}
}

View File

@@ -1,15 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
using Svg.Skia;
using System.Collections.Specialized; using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -89,12 +85,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Get all pages in the navigation stack // Get all pages in the navigation stack
var pages = VirtualView.Navigation.NavigationStack.ToList(); var pages = VirtualView.Navigation.NavigationStack.ToList();
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up {pages.Count} pages"); Console.WriteLine($"[NavigationPageHandler] Setting up {pages.Count} pages");
// If no pages in stack, check CurrentPage // If no pages in stack, check CurrentPage
if (pages.Count == 0 && VirtualView.CurrentPage != null) if (pages.Count == 0 && VirtualView.CurrentPage != null)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}"); Console.WriteLine($"[NavigationPageHandler] No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
pages.Add(VirtualView.CurrentPage); pages.Add(VirtualView.CurrentPage);
} }
@@ -103,12 +99,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Ensure the page has a handler // Ensure the page has a handler
if (page.Handler == null) if (page.Handler == null)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for: {page.Title}"); Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
page.Handler = page.ToViewHandler(MauiContext); page.Handler = page.ToHandler(MauiContext);
} }
DiagnosticLog.Debug("NavigationPageHandler", $"Page handler type: {page.Handler?.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
DiagnosticLog.Debug("NavigationPageHandler", $"Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
if (page.Handler?.PlatformView is SkiaPage skiaPage) if (page.Handler?.PlatformView is SkiaPage skiaPage)
{ {
@@ -118,20 +114,20 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
skiaPage.TitleTextColor = PlatformView.BarTextColor; skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = page.Title ?? ""; skiaPage.Title = page.Title ?? "";
DiagnosticLog.Debug("NavigationPageHandler", $"SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}"); Console.WriteLine($"[NavigationPageHandler] SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
// If content is null, try to get it from ContentPage // If content is null, try to get it from ContentPage
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null) if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, manually creating handler for: {contentPage.Content.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null) if (contentPage.Content.Handler == null)
{ {
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext); contentPage.Content.Handler = contentPage.Content.ToHandler(MauiContext);
} }
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent) if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{ {
skiaPage.Content = skiaContent; skiaPage.Content = skiaContent;
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
} }
} }
@@ -140,18 +136,18 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (PlatformView.StackDepth == 0) if (PlatformView.StackDepth == 0)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Setting root page: {page.Title}"); Console.WriteLine($"[NavigationPageHandler] Setting root page: {page.Title}");
PlatformView.SetRootPage(skiaPage); PlatformView.SetRootPage(skiaPage);
} }
else else
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Pushing page: {page.Title}"); Console.WriteLine($"[NavigationPageHandler] Pushing page: {page.Title}");
PlatformView.Push(skiaPage, false); PlatformView.Push(skiaPage, false);
} }
} }
else else
{ {
DiagnosticLog.Warn("NavigationPageHandler", $"Failed to get SkiaPage for: {page.Title}"); Console.WriteLine($"[NavigationPageHandler] Failed to get SkiaPage for: {page.Title}");
} }
} }
} }
@@ -162,12 +158,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
{ {
if (skiaPage is SkiaContentPage contentPage) if (skiaPage is SkiaContentPage contentPage)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}"); Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
contentPage.ToolbarItems.Clear(); contentPage.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems) foreach (var item in page.ToolbarItems)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Adding toolbar item: '{item.Text}', IconImageSource={item.IconImageSource}, Order={item.Order}"); Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', Order={item.Order}");
// Default and Primary should both be treated as Primary (shown in toolbar) // Default and Primary should both be treated as Primary (shown in toolbar)
// Only Secondary goes to overflow menu // Only Secondary goes to overflow menu
var order = item.Order == ToolbarItemOrder.Secondary var order = item.Order == ToolbarItemOrder.Secondary
@@ -178,7 +174,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
var toolbarItem = item; // Capture for closure var toolbarItem = item; // Capture for closure
var clickCommand = new RelayCommand(() => var clickCommand = new RelayCommand(() =>
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItem '{toolbarItem.Text}' clicked, invoking..."); Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
// Use IMenuItemController to send the click // Use IMenuItemController to send the click
if (toolbarItem is IMenuItemController menuController) if (toolbarItem is IMenuItemController menuController)
{ {
@@ -191,17 +187,9 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
} }
}); });
// Load icon if specified
SKBitmap? icon = null;
if (item.IconImageSource is FileImageSource fileSource && !string.IsNullOrEmpty(fileSource.File))
{
icon = LoadToolbarIcon(fileSource.File);
}
contentPage.ToolbarItems.Add(new SkiaToolbarItem contentPage.ToolbarItems.Add(new SkiaToolbarItem
{ {
Text = item.Text ?? "", Text = item.Text ?? "",
Icon = icon,
Order = order, Order = order,
Command = clickCommand Command = clickCommand
}); });
@@ -210,10 +198,10 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Subscribe to ToolbarItems changes if not already subscribed // Subscribe to ToolbarItems changes if not already subscribed
if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page)) if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page))
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Subscribing to ToolbarItems changes for '{page.Title}'"); Console.WriteLine($"[NavigationPageHandler] Subscribing to ToolbarItems changes for '{page.Title}'");
notifyCollection.CollectionChanged += (s, e) => notifyCollection.CollectionChanged += (s, e) =>
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItems changed for '{page.Title}', action={e.Action}"); Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}");
MapToolbarItems(skiaPage, page); MapToolbarItems(skiaPage, page);
skiaPage.Invalidate(); skiaPage.Invalidate();
}; };
@@ -222,120 +210,53 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
} }
} }
private SKBitmap? LoadToolbarIcon(string fileName)
{
try
{
string baseDirectory = AppContext.BaseDirectory;
string pngPath = Path.Combine(baseDirectory, fileName);
string svgPath = Path.Combine(baseDirectory, Path.ChangeExtension(fileName, ".svg"));
DiagnosticLog.Debug("NavigationPageHandler", $"LoadToolbarIcon: Looking for {fileName}");
DiagnosticLog.Debug("NavigationPageHandler", $" Trying PNG: {pngPath} (exists: {File.Exists(pngPath)})");
DiagnosticLog.Debug("NavigationPageHandler", $" Trying SVG: {svgPath} (exists: {File.Exists(svgPath)})");
// Try SVG first
if (File.Exists(svgPath))
{
using var svg = new SKSvg();
svg.Load(svgPath);
if (svg.Picture != null)
{
var cullRect = svg.Picture.CullRect;
float scale = 24f / Math.Max(cullRect.Width, cullRect.Height);
var bitmap = new SKBitmap(24, 24, false);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
canvas.Scale(scale);
canvas.DrawPicture(svg.Picture, null);
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded SVG icon: {svgPath}");
return bitmap;
}
}
// Try PNG
if (File.Exists(pngPath))
{
using var stream = File.OpenRead(pngPath);
var result = SKBitmap.Decode(stream);
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded PNG icon: {pngPath}");
return result;
}
DiagnosticLog.Warn("NavigationPageHandler", $"Icon not found: {fileName}");
return null;
}
catch (Exception ex)
{
DiagnosticLog.Error("NavigationPageHandler", $"Error loading icon {fileName}: {ex.Message}", ex);
return null;
}
}
private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{ {
try try
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Pushed: {e.Page?.Title}"); Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}");
if (e.Page == null || PlatformView == null || MauiContext == null) return; if (e.Page == null || PlatformView == null || MauiContext == null) return;
// Ensure the page has a handler // Ensure the page has a handler
if (e.Page.Handler == null) if (e.Page.Handler == null)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for page: {e.Page.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
e.Page.Handler = e.Page.ToViewHandler(MauiContext); e.Page.Handler = e.Page.ToHandler(MauiContext);
DiagnosticLog.Debug("NavigationPageHandler", $"Handler created: {e.Page.Handler?.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
} }
if (e.Page.Handler?.PlatformView is SkiaPage skiaPage) if (e.Page.Handler?.PlatformView is SkiaPage skiaPage)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}"); Console.WriteLine($"[NavigationPageHandler] Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
skiaPage.ShowNavigationBar = true; skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor; skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor; skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = e.Page.Title ?? ""; Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items");
// Handle content if null
if (skiaPage.Content == null && e.Page is ContentPage contentPage && contentPage.Content != null)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
skiaPage.Content = skiaContent;
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}");
}
}
DiagnosticLog.Debug("NavigationPageHandler", "Mapping toolbar items");
MapToolbarItems(skiaPage, e.Page); MapToolbarItems(skiaPage, e.Page);
DiagnosticLog.Debug("NavigationPageHandler", "Pushing page to platform"); Console.WriteLine($"[NavigationPageHandler] Pushing page to platform");
PlatformView.Push(skiaPage, false); PlatformView.Push(skiaPage, true);
DiagnosticLog.Debug("NavigationPageHandler", $"Push complete, thread={Environment.CurrentManagedThreadId}"); Console.WriteLine($"[NavigationPageHandler] Push complete");
} }
DiagnosticLog.Debug("NavigationPageHandler", "OnVirtualViewPushed returning");
} }
catch (Exception ex) catch (Exception ex)
{ {
DiagnosticLog.Error("NavigationPageHandler", $"EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}", ex); Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}");
throw; throw;
} }
} }
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{ {
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Popped: {e.Page?.Title}"); Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}");
// Pop on the platform side to sync with MAUI navigation // Pop on the platform side to sync with MAUI navigation
PlatformView?.Pop(); PlatformView?.Pop(true);
} }
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{ {
DiagnosticLog.Debug("NavigationPageHandler", "VirtualView PoppedToRoot"); Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot");
PlatformView?.PopToRoot(); PlatformView?.PopToRoot(true);
} }
private void OnPushed(object? sender, NavigationEventArgs e) private void OnPushed(object? sender, NavigationEventArgs e)
@@ -364,7 +285,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarBackgroundColor is not null) if (navigationPage.BarBackgroundColor is not null)
{ {
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor; handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor();
} }
} }
@@ -374,7 +295,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarBackground is SolidColorBrush solidBrush) if (navigationPage.BarBackground is SolidColorBrush solidBrush)
{ {
handler.PlatformView.BarBackgroundColor = solidBrush.Color; handler.PlatformView.BarBackgroundColor = solidBrush.Color.ToSKColor();
} }
} }
@@ -384,7 +305,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarTextColor is not null) if (navigationPage.BarTextColor is not null)
{ {
handler.PlatformView.BarTextColor = navigationPage.BarTextColor; handler.PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor();
} }
} }
@@ -394,7 +315,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.Background is SolidColorBrush solidBrush) if (navigationPage.Background is SolidColorBrush solidBrush)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
} }
} }
@@ -403,7 +324,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request) if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request)
return; return;
DiagnosticLog.Debug("NavigationPageHandler", $"MapRequestNavigation: {request.NavigationStack.Count} pages"); Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {request.NavigationStack.Count} pages");
// Handle navigation request // Handle navigation request
foreach (var view in request.NavigationStack) foreach (var view in request.NavigationStack)
@@ -413,7 +334,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Ensure handler exists // Ensure handler exists
if (page.Handler == null) if (page.Handler == null)
{ {
page.Handler = page.ToViewHandler(handler.MauiContext); page.Handler = page.ToHandler(handler.MauiContext);
} }
if (page.Handler?.PlatformView is SkiaPage skiaPage) if (page.Handler?.PlatformView is SkiaPage skiaPage)

View File

@@ -5,8 +5,6 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -21,11 +19,8 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
{ {
[nameof(Page.Title)] = MapTitle, [nameof(Page.Title)] = MapTitle,
[nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource, [nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource,
[nameof(Page.IconImageSource)] = MapIconImageSource,
[nameof(Page.Padding)] = MapPadding, [nameof(Page.Padding)] = MapPadding,
[nameof(Page.IsBusy)] = MapIsBusy,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(VisualElement.BackgroundColor)] = MapBackgroundColor,
}; };
public static CommandMapper<Page, PageHandler> CommandMapper = public static CommandMapper<Page, PageHandler> CommandMapper =
@@ -50,10 +45,6 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
protected override void ConnectHandler(SkiaPage platformView) protected override void ConnectHandler(SkiaPage platformView)
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
// Set MauiPage reference for theme refresh support
platformView.MauiPage = VirtualView;
platformView.Appearing += OnAppearing; platformView.Appearing += OnAppearing;
platformView.Disappearing += OnDisappearing; platformView.Disappearing += OnDisappearing;
} }
@@ -62,13 +53,12 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
{ {
platformView.Appearing -= OnAppearing; platformView.Appearing -= OnAppearing;
platformView.Disappearing -= OnDisappearing; platformView.Disappearing -= OnDisappearing;
platformView.MauiPage = null;
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnAppearing(object? sender, EventArgs e) private void OnAppearing(object? sender, EventArgs e)
{ {
DiagnosticLog.Debug("PageHandler", $"OnAppearing received for: {VirtualView?.Title}"); Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}");
(VirtualView as IPageController)?.SendAppearing(); (VirtualView as IPageController)?.SendAppearing();
} }
@@ -107,34 +97,9 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
if (page.Background is SolidColorBrush solidBrush) if (page.Background is SolidColorBrush solidBrush)
{ {
handler.PlatformView.BackgroundColor = solidBrush.Color; handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
} }
} }
public static void MapBackgroundColor(PageHandler handler, Page page)
{
if (handler.PlatformView is null) return;
var backgroundColor = page.BackgroundColor;
if (backgroundColor != null && backgroundColor != Colors.Transparent)
{
handler.PlatformView.BackgroundColor = backgroundColor;
DiagnosticLog.Debug("PageHandler", $"MapBackgroundColor: {backgroundColor}");
}
}
public static void MapIconImageSource(PageHandler handler, Page page)
{
// Icon is typically used by navigation containers (Shell, TabbedPage)
// Store for later use but don't render directly on the page
handler.PlatformView?.Invalidate();
}
public static void MapIsBusy(PageHandler handler, Page page)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsBusy = page.IsBusy;
}
} }
/// <summary> /// <summary>
@@ -146,7 +111,6 @@ public partial class ContentPageHandler : PageHandler
new PropertyMapper<ContentPage, ContentPageHandler>(PageHandler.Mapper) new PropertyMapper<ContentPage, ContentPageHandler>(PageHandler.Mapper)
{ {
[nameof(ContentPage.Content)] = MapContent, [nameof(ContentPage.Content)] = MapContent,
[nameof(ContentPage.ToolbarItems)] = MapToolbarItems,
}; };
public static new CommandMapper<ContentPage, ContentPageHandler> CommandMapper = public static new CommandMapper<ContentPage, ContentPageHandler> CommandMapper =
@@ -168,17 +132,6 @@ public partial class ContentPageHandler : PageHandler
return new SkiaContentPage(); return new SkiaContentPage();
} }
protected override void ConnectHandler(SkiaPage platformView)
{
base.ConnectHandler(platformView);
// Sync toolbar items initially
if (VirtualView is ContentPage contentPage && platformView is SkiaContentPage skiaContentPage)
{
SyncToolbarItems(skiaContentPage, contentPage);
}
}
public static void MapContent(ContentPageHandler handler, ContentPage page) public static void MapContent(ContentPageHandler handler, ContentPage page)
{ {
if (handler.PlatformView is null || handler.MauiContext is null) return; if (handler.PlatformView is null || handler.MauiContext is null) return;
@@ -190,19 +143,19 @@ public partial class ContentPageHandler : PageHandler
// Create handler for content if it doesn't exist // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
DiagnosticLog.Debug("ContentPageHandler", $"Creating handler for content: {content.GetType().Name}"); Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToViewHandler(handler.MauiContext); content.Handler = content.ToHandler(handler.MauiContext);
} }
// The content's handler should provide the platform view // The content's handler should provide the platform view
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)
{ {
DiagnosticLog.Debug("ContentPageHandler", $"Setting content: {skiaContent.GetType().Name}"); Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent; handler.PlatformView.Content = skiaContent;
} }
else else
{ {
DiagnosticLog.Warn("ContentPageHandler", $"Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}"); Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
} }
} }
else else
@@ -210,38 +163,4 @@ public partial class ContentPageHandler : PageHandler
handler.PlatformView.Content = null; handler.PlatformView.Content = null;
} }
} }
public static void MapToolbarItems(ContentPageHandler handler, ContentPage page)
{
if (handler.PlatformView is not SkiaContentPage skiaContentPage) return;
SyncToolbarItems(skiaContentPage, page);
}
private static void SyncToolbarItems(SkiaContentPage platformView, ContentPage page)
{
platformView.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems)
{
var skiaItem = new SkiaToolbarItem
{
Text = item.Text ?? "",
Command = item.Command,
Order = item.Order == ToolbarItemOrder.Primary
? SkiaToolbarItemOrder.Primary
: SkiaToolbarItemOrder.Secondary
};
// Load icon if present
if (item.IconImageSource is FileImageSource fileSource)
{
// Icon loading would be async - simplified for now
DiagnosticLog.Debug("ContentPageHandler", $"Toolbar item icon: {fileSource.File}");
}
platformView.ToolbarItems.Add(skiaItem);
}
platformView.Invalidate();
}
} }

View File

@@ -5,13 +5,13 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using SkiaSharp;
using System.Collections.Specialized; using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary> /// <summary>
/// Handler for Picker on Linux using Skia rendering. /// Handler for Picker on Linux using Skia rendering.
/// Maps IPicker interface to SkiaPicker platform view.
/// </summary> /// </summary>
public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker> public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
{ {
@@ -22,12 +22,10 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
[nameof(IPicker.TitleColor)] = MapTitleColor, [nameof(IPicker.TitleColor)] = MapTitleColor,
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex, [nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
[nameof(IPicker.TextColor)] = MapTextColor, [nameof(IPicker.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing, [nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(Picker.ItemsSource)] = MapItemsSource, [nameof(Picker.ItemsSource)] = MapItemsSource,
}; };
@@ -64,17 +62,8 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
_itemsCollection.CollectionChanged += OnItemsCollectionChanged; _itemsCollection.CollectionChanged += OnItemsCollectionChanged;
} }
// Load items and sync properties // Load items
ReloadItems(); ReloadItems();
if (VirtualView != null)
{
MapTitle(this, VirtualView);
MapTitleColor(this, VirtualView);
MapTextColor(this, VirtualView);
MapSelectedIndex(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
} }
protected override void DisconnectHandler(SkiaPicker platformView) protected override void DisconnectHandler(SkiaPicker platformView)
@@ -95,14 +84,11 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
ReloadItems(); ReloadItems();
} }
private void OnSelectedIndexChanged(object? sender, SelectedIndexChangedEventArgs e) private void OnSelectedIndexChanged(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
if (VirtualView.SelectedIndex != e.NewIndex) VirtualView.SelectedIndex = PlatformView.SelectedIndex;
{
VirtualView.SelectedIndex = e.NewIndex;
}
} }
private void ReloadItems() private void ReloadItems()
@@ -124,68 +110,38 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (picker.TitleColor is not null) if (picker.TitleColor is not null)
{ {
handler.PlatformView.TitleColor = picker.TitleColor; handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor();
} }
} }
public static void MapSelectedIndex(PickerHandler handler, IPicker picker) public static void MapSelectedIndex(PickerHandler handler, IPicker picker)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (handler.PlatformView.SelectedIndex != picker.SelectedIndex)
{
handler.PlatformView.SelectedIndex = picker.SelectedIndex; handler.PlatformView.SelectedIndex = picker.SelectedIndex;
} }
}
public static void MapTextColor(PickerHandler handler, IPicker picker) public static void MapTextColor(PickerHandler handler, IPicker picker)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (picker.TextColor is not null) if (picker.TextColor is not null)
{ {
handler.PlatformView.TextColor = picker.TextColor; handler.PlatformView.TextColor = picker.TextColor.ToSKColor();
} }
} }
public static void MapFont(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView is null) return;
var font = picker.Font;
if (!string.IsNullOrEmpty(font.Family))
{
handler.PlatformView.FontFamily = font.Family;
}
if (font.Size > 0)
{
handler.PlatformView.FontSize = font.Size;
}
// Map FontAttributes from the Font weight
var attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
handler.PlatformView.FontAttributes = attrs;
handler.PlatformView.Invalidate();
}
public static void MapCharacterSpacing(PickerHandler handler, IPicker picker) public static void MapCharacterSpacing(PickerHandler handler, IPicker picker)
{ {
if (handler.PlatformView is null) return; // Character spacing could be implemented with custom text rendering
handler.PlatformView.CharacterSpacing = picker.CharacterSpacing;
} }
public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker) public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker)
{ {
if (handler.PlatformView is null) return; // Text alignment would require changes to SkiaPicker drawing
handler.PlatformView.HorizontalTextAlignment = picker.HorizontalTextAlignment;
} }
public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker) public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker)
{ {
if (handler.PlatformView is null) return; // Text alignment would require changes to SkiaPicker drawing
handler.PlatformView.VerticalTextAlignment = picker.VerticalTextAlignment;
} }
public static void MapBackground(PickerHandler handler, IPicker picker) public static void MapBackground(PickerHandler handler, IPicker picker)
@@ -194,17 +150,10 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
if (picker.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (picker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
public static void MapIsEnabled(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = picker.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapItemsSource(PickerHandler handler, IPicker picker) public static void MapItemsSource(PickerHandler handler, IPicker picker)
{ {
handler.ReloadItems(); handler.ReloadItems();

View File

@@ -0,0 +1,63 @@
// 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;
/// <summary>
/// Linux handler for ProgressBar control.
/// </summary>
public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar>
{
public static IPropertyMapper<IProgress, ProgressBarHandler> Mapper = new PropertyMapper<IProgress, ProgressBarHandler>(ViewHandler.ViewMapper)
{
[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);
public ProgressBarHandler() : base(Mapper, CommandMapper) { }
protected override SkiaProgressBar CreatePlatformView() => new SkiaProgressBar();
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
{
handler.PlatformView.Progress = progress.Progress;
}
public static void MapProgressColor(ProgressBarHandler handler, IProgress progress)
{
if (progress.ProgressColor != null)
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress)
{
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

@@ -1,11 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -20,12 +18,7 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
{ {
[nameof(IProgress.Progress)] = MapProgress, [nameof(IProgress.Progress)] = MapProgress,
[nameof(IProgress.ProgressColor)] = MapProgressColor, [nameof(IProgress.ProgressColor)] = MapProgressColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
[nameof(IView.Height)] = MapHeight,
[nameof(IView.Width)] = MapWidth,
["VerticalOptions"] = MapVerticalOptions,
}; };
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -46,48 +39,6 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
return new SkiaProgressBar(); return new SkiaProgressBar();
} }
protected override void ConnectHandler(SkiaProgressBar platformView)
{
base.ConnectHandler(platformView);
if (VirtualView is BindableObject bindable)
{
bindable.PropertyChanged += OnVirtualViewPropertyChanged;
}
if (VirtualView is VisualElement visualElement)
{
platformView.IsVisible = visualElement.IsVisible;
}
// Sync properties
if (VirtualView != null)
{
MapProgress(this, VirtualView);
MapProgressColor(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaProgressBar platformView)
{
if (VirtualView is BindableObject bindable)
{
bindable.PropertyChanged -= OnVirtualViewPropertyChanged;
}
base.DisconnectHandler(platformView);
}
private void OnVirtualViewPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (VirtualView is VisualElement visualElement && e.PropertyName == nameof(VisualElement.IsVisible))
{
PlatformView.IsVisible = visualElement.IsVisible;
PlatformView.Invalidate();
}
}
public static void MapProgress(ProgressBarHandler handler, IProgress progress) public static void MapProgress(ProgressBarHandler handler, IProgress progress)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
@@ -99,18 +50,7 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (progress.ProgressColor is not null) if (progress.ProgressColor is not null)
{ handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
handler.PlatformView.ProgressColor = progress.ProgressColor;
}
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = progress.IsEnabled;
handler.PlatformView.Invalidate();
} }
public static void MapBackground(ProgressBarHandler handler, IProgress progress) public static void MapBackground(ProgressBarHandler handler, IProgress progress)
@@ -119,49 +59,7 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress)
{
if (handler.PlatformView is null) return;
if (progress is VisualElement visualElement && visualElement.BackgroundColor is not null)
{
handler.PlatformView.TrackColor = visualElement.BackgroundColor;
handler.PlatformView.Invalidate();
}
}
public static void MapHeight(ProgressBarHandler handler, IProgress progress)
{
if (handler.PlatformView is null) return;
if (progress is VisualElement visualElement && visualElement.HeightRequest >= 0)
{
handler.PlatformView.HeightRequest = visualElement.HeightRequest;
}
}
public static void MapWidth(ProgressBarHandler handler, IProgress progress)
{
if (handler.PlatformView is null) return;
if (progress is VisualElement visualElement && visualElement.WidthRequest >= 0)
{
handler.PlatformView.WidthRequest = visualElement.WidthRequest;
}
}
public static void MapVerticalOptions(ProgressBarHandler handler, IProgress progress)
{
if (handler.PlatformView is null) return;
if (progress is Microsoft.Maui.Controls.View view)
{
handler.PlatformView.VerticalOptions = view.VerticalOptions;
} }
} }
} }

View File

@@ -80,7 +80,7 @@ public partial class RadioButtonHandler : ViewHandler<IRadioButton, SkiaRadioBut
if (radioButton.TextColor is not null) if (radioButton.TextColor is not null)
{ {
handler.PlatformView.TextColor = radioButton.TextColor; handler.PlatformView.TextColor = radioButton.TextColor.ToSKColor();
} }
} }
@@ -100,7 +100,7 @@ public partial class RadioButtonHandler : ViewHandler<IRadioButton, SkiaRadioBut
if (radioButton.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (radioButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
} }

View File

@@ -1,151 +0,0 @@
// 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.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for RefreshView on Linux using Skia rendering.
/// Maps RefreshView to SkiaRefreshView platform view.
/// </summary>
public partial class RefreshViewHandler : ViewHandler<RefreshView, SkiaRefreshView>
{
private bool _isUpdatingRefreshing;
public static IPropertyMapper<RefreshView, RefreshViewHandler> Mapper =
new PropertyMapper<RefreshView, RefreshViewHandler>(ViewHandler.ViewMapper)
{
[nameof(RefreshView.Content)] = MapContent,
[nameof(RefreshView.IsRefreshing)] = MapIsRefreshing,
[nameof(RefreshView.RefreshColor)] = MapRefreshColor,
[nameof(RefreshView.Command)] = MapCommand,
[nameof(RefreshView.CommandParameter)] = MapCommandParameter,
[nameof(IView.Background)] = MapBackground,
};
public static CommandMapper<RefreshView, RefreshViewHandler> CommandMapper =
new(ViewHandler.ViewCommandMapper)
{
};
public RefreshViewHandler() : base(Mapper, CommandMapper)
{
}
public RefreshViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaRefreshView CreatePlatformView()
{
return new SkiaRefreshView();
}
protected override void ConnectHandler(SkiaRefreshView platformView)
{
base.ConnectHandler(platformView);
platformView.Refreshing += OnRefreshing;
}
protected override void DisconnectHandler(SkiaRefreshView platformView)
{
platformView.Refreshing -= OnRefreshing;
base.DisconnectHandler(platformView);
}
private void OnRefreshing(object? sender, EventArgs e)
{
if (VirtualView is null || _isUpdatingRefreshing) return;
try
{
_isUpdatingRefreshing = true;
// Notify the virtual view that refreshing has started
VirtualView.IsRefreshing = true;
// The command will be executed by the platform view
}
finally
{
_isUpdatingRefreshing = false;
}
}
public static void MapContent(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var content = refreshView.Content;
if (content == null)
{
handler.PlatformView.Content = null;
return;
}
// Create handler for content
if (content.Handler == null)
{
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
handler.PlatformView.Content = skiaContent;
}
}
public static void MapIsRefreshing(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null || handler._isUpdatingRefreshing) return;
try
{
handler._isUpdatingRefreshing = true;
handler.PlatformView.IsRefreshing = refreshView.IsRefreshing;
}
finally
{
handler._isUpdatingRefreshing = false;
}
}
public static void MapRefreshColor(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null) return;
if (refreshView.RefreshColor is not null)
{
handler.PlatformView.RefreshColor = refreshView.RefreshColor;
}
}
public static void MapCommand(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Command = refreshView.Command;
}
public static void MapCommandParameter(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.CommandParameter = refreshView.CommandParameter;
}
public static void MapBackground(RefreshViewHandler handler, RefreshView refreshView)
{
if (handler.PlatformView is null) return;
if (refreshView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.RefreshBackgroundColor = solidBrush.Color;
}
}
}

View File

@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -49,17 +47,17 @@ public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView
var content = scrollView.PresentedContent; var content = scrollView.PresentedContent;
if (content != null) if (content != null)
{ {
DiagnosticLog.Debug("ScrollViewHandler", $"MapContent: {content.GetType().Name}"); Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}");
// Create handler for content if it doesn't exist // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
content.Handler = content.ToViewHandler(handler.MauiContext); content.Handler = content.ToHandler(handler.MauiContext);
} }
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)
{ {
DiagnosticLog.Debug("ScrollViewHandler", $"Setting content: {skiaContent.GetType().Name}"); Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent; handler.PlatformView.Content = skiaContent;
} }
} }

View File

@@ -0,0 +1,106 @@
// 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;
/// <summary>
/// Linux handler for SearchBar control.
/// </summary>
public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
{
public static IPropertyMapper<ISearchBar, SearchBarHandler> Mapper = new PropertyMapper<ISearchBar, SearchBarHandler>(ViewHandler.ViewMapper)
{
[nameof(ISearchBar.Text)] = MapText,
[nameof(ISearchBar.Placeholder)] = MapPlaceholder,
[nameof(ISearchBar.PlaceholderColor)] = MapPlaceholderColor,
[nameof(ISearchBar.TextColor)] = MapTextColor,
[nameof(ISearchBar.Font)] = MapFont,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
};
public static CommandMapper<ISearchBar, SearchBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SearchBarHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSearchBar CreatePlatformView() => new SkiaSearchBar();
protected override void ConnectHandler(SkiaSearchBar platformView)
{
base.ConnectHandler(platformView);
platformView.TextChanged += OnTextChanged;
platformView.SearchButtonPressed += OnSearchButtonPressed;
}
protected override void DisconnectHandler(SkiaSearchBar platformView)
{
platformView.TextChanged -= OnTextChanged;
platformView.SearchButtonPressed -= OnSearchButtonPressed;
base.DisconnectHandler(platformView);
}
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (VirtualView != null && VirtualView.Text != e.NewText)
{
VirtualView.Text = e.NewText;
}
}
private void OnSearchButtonPressed(object? sender, EventArgs e)
{
VirtualView?.SearchButtonPressed();
}
public static void MapText(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView.Text != searchBar.Text)
{
handler.PlatformView.Text = searchBar.Text ?? "";
}
}
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
{
handler.PlatformView.Placeholder = searchBar.Placeholder ?? "";
handler.PlatformView.Invalidate();
}
public static void MapPlaceholderColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.PlaceholderColor != null)
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.TextColor != null)
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
{
var font = searchBar.Font;
if (font.Family != null)
handler.PlatformView.FontFamily = font.Family;
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SearchBarHandler handler, ISearchBar searchBar)
{
handler.PlatformView.IsEnabled = searchBar.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}

View File

@@ -18,11 +18,9 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
[nameof(ITextInput.Text)] = MapText, [nameof(ITextInput.Text)] = MapText,
[nameof(ITextStyle.TextColor)] = MapTextColor, [nameof(ITextStyle.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont, [nameof(ITextStyle.Font)] = MapFont,
[nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IPlaceholder.Placeholder)] = MapPlaceholder, [nameof(IPlaceholder.Placeholder)] = MapPlaceholder,
[nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor, [nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor,
[nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor, [nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor,
[nameof(ISearchBar.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
}; };
@@ -86,7 +84,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (searchBar.TextColor is not null) if (searchBar.TextColor is not null)
handler.PlatformView.TextColor = searchBar.TextColor; handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
} }
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar) public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
@@ -95,28 +93,10 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
var font = searchBar.Font; var font = searchBar.Font;
if (font.Size > 0) if (font.Size > 0)
handler.PlatformView.FontSize = font.Size; handler.PlatformView.FontSize = (float)font.Size;
if (!string.IsNullOrEmpty(font.Family)) if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family; handler.PlatformView.FontFamily = font.Family;
// Map FontAttributes from the Font weight
var attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
handler.PlatformView.FontAttributes = attrs;
}
public static void MapCharacterSpacing(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = searchBar.CharacterSpacing;
}
public static void MapHorizontalTextAlignment(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalTextAlignment = searchBar.HorizontalTextAlignment;
} }
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar) public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
@@ -130,7 +110,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (searchBar.PlaceholderColor is not null) if (searchBar.PlaceholderColor is not null)
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor; handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
} }
public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar) public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)
@@ -139,7 +119,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
// CancelButtonColor maps to ClearButtonColor // CancelButtonColor maps to ClearButtonColor
if (searchBar.CancelButtonColor is not null) if (searchBar.CancelButtonColor is not null)
handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor; handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor.ToSKColor();
} }
@@ -149,7 +129,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (searchBar.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (searchBar.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
} }

View File

@@ -4,8 +4,6 @@
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -15,27 +13,12 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary> /// </summary>
public partial class ShellHandler : ViewHandler<Shell, SkiaShell> public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{ {
private bool _isUpdatingFlyoutPresented;
public static IPropertyMapper<Shell, ShellHandler> Mapper = new PropertyMapper<Shell, ShellHandler>(ViewHandler.ViewMapper) public static IPropertyMapper<Shell, ShellHandler> Mapper = new PropertyMapper<Shell, ShellHandler>(ViewHandler.ViewMapper)
{ {
[nameof(Shell.FlyoutIsPresented)] = MapFlyoutIsPresented,
[nameof(Shell.FlyoutBehavior)] = MapFlyoutBehavior,
[nameof(Shell.FlyoutWidth)] = MapFlyoutWidth,
[nameof(Shell.FlyoutBackgroundColor)] = MapFlyoutBackgroundColor,
[nameof(Shell.FlyoutBackground)] = MapFlyoutBackground,
[nameof(Shell.BackgroundColor)] = MapBackgroundColor,
[nameof(Shell.FlyoutHeaderBehavior)] = MapFlyoutHeaderBehavior,
[nameof(Shell.FlyoutHeader)] = MapFlyoutHeader,
[nameof(Shell.FlyoutFooter)] = MapFlyoutFooter,
[nameof(Shell.Items)] = MapItems,
[nameof(Shell.CurrentItem)] = MapCurrentItem,
[nameof(Shell.Title)] = MapTitle,
}; };
public static CommandMapper<Shell, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<Shell, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{ {
["GoToAsync"] = MapGoToAsync,
}; };
public ShellHandler() : base(Mapper, CommandMapper) public ShellHandler() : base(Mapper, CommandMapper)
@@ -49,32 +32,20 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
protected override SkiaShell CreatePlatformView() protected override SkiaShell CreatePlatformView()
{ {
DiagnosticLog.Debug("ShellHandler", "CreatePlatformView - creating SkiaShell");
return new SkiaShell(); return new SkiaShell();
} }
protected override void ConnectHandler(SkiaShell platformView) protected override void ConnectHandler(SkiaShell platformView)
{ {
DiagnosticLog.Debug("ShellHandler", "ConnectHandler - connecting to SkiaShell");
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
platformView.Navigated += OnNavigated; platformView.Navigated += OnNavigated;
// Store reference to MAUI Shell for callbacks
platformView.MauiShell = VirtualView;
// Set up content renderer
platformView.ContentRenderer = RenderShellContent;
platformView.ColorRefresher = RefreshShellColors;
// Subscribe to Shell navigation events // Subscribe to Shell navigation events
if (VirtualView != null) if (VirtualView != null)
{ {
VirtualView.Navigating += OnShellNavigating; VirtualView.Navigating += OnShellNavigating;
VirtualView.Navigated += OnShellNavigated; VirtualView.Navigated += OnShellNavigated;
// Initial sync of shell items
SyncShellItems();
} }
} }
@@ -82,9 +53,6 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{ {
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged; platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
platformView.Navigated -= OnNavigated; platformView.Navigated -= OnNavigated;
platformView.MauiShell = null;
platformView.ContentRenderer = null;
platformView.ColorRefresher = null;
if (VirtualView != null) if (VirtualView != null)
{ {
@@ -97,324 +65,29 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e) private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null || _isUpdatingFlyoutPresented) return; // Sync flyout state to virtual view
try
{
_isUpdatingFlyoutPresented = true;
VirtualView.FlyoutIsPresented = PlatformView.FlyoutIsPresented;
}
finally
{
_isUpdatingFlyoutPresented = false;
}
} }
private void OnNavigated(object? sender, Platform.ShellNavigationEventArgs e) private void OnNavigated(object? sender, ShellNavigationEventArgs e)
{ {
// Handle platform navigation events // Handle platform navigation events
} }
private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e) private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e)
{ {
DiagnosticLog.Debug("ShellHandler", $"Shell Navigating to: {e.Target?.Location}"); Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}");
// Route to platform view // Route to platform view
if (PlatformView != null && e.Target?.Location != null) if (PlatformView != null && e.Target?.Location != null)
{ {
var route = e.Target.Location.ToString().TrimStart('/'); var route = e.Target.Location.ToString().TrimStart('/');
DiagnosticLog.Debug("ShellHandler", $"Routing to: {route}"); Console.WriteLine($"[ShellHandler] Routing to: {route}");
PlatformView.GoToAsync(route); PlatformView.GoToAsync(route);
} }
} }
private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{ {
DiagnosticLog.Debug("ShellHandler", $"Shell Navigated to: {e.Current?.Location}"); Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}");
}
private void SyncShellItems()
{
if (PlatformView is null || VirtualView is null || MauiContext is null) return;
// Clear existing sections
foreach (var section in PlatformView.Sections.ToList())
{
PlatformView.RemoveSection(section);
}
// Add shell items as sections
foreach (var item in VirtualView.Items)
{
if (item is FlyoutItem flyoutItem)
{
var section = new Platform.ShellSection
{
Route = flyoutItem.Route ?? flyoutItem.Title ?? "",
Title = flyoutItem.Title ?? "",
IconPath = flyoutItem.Icon?.ToString()
};
// Add shell contents as items
foreach (var shellSection in flyoutItem.Items)
{
foreach (var content in shellSection.Items)
{
var contentItem = new Platform.ShellContent
{
Route = content.Route ?? content.Title ?? "",
Title = content.Title ?? "",
IconPath = content.Icon?.ToString(),
MauiShellContent = content,
Content = RenderShellContent(content)
};
section.Items.Add(contentItem);
}
}
PlatformView.AddSection(section);
}
else if (item is ShellItem shellItem)
{
var section = new Platform.ShellSection
{
Route = shellItem.Route ?? shellItem.Title ?? "",
Title = shellItem.Title ?? "",
IconPath = shellItem.Icon?.ToString()
};
foreach (var shellSection in shellItem.Items)
{
foreach (var content in shellSection.Items)
{
var contentItem = new Platform.ShellContent
{
Route = content.Route ?? content.Title ?? "",
Title = content.Title ?? "",
IconPath = content.Icon?.ToString(),
MauiShellContent = content,
Content = RenderShellContent(content)
};
section.Items.Add(contentItem);
}
}
PlatformView.AddSection(section);
}
}
}
private SkiaView? RenderShellContent(Microsoft.Maui.Controls.ShellContent content)
{
if (MauiContext is null) return null;
try
{
var page = content.Content as Page;
if (page == null && content.ContentTemplate != null)
{
page = content.ContentTemplate.CreateContent() as Page;
}
if (page != null)
{
if (page.Handler == null)
{
page.Handler = page.ToViewHandler(MauiContext);
}
if (page.Handler?.PlatformView is SkiaView skiaView)
{
return skiaView;
}
}
}
catch (Exception ex)
{
DiagnosticLog.Error("ShellHandler", $"Error rendering content: {ex.Message}", ex);
}
return null;
}
private static void RefreshShellColors(SkiaShell platformView, Shell shell)
{
// Sync flyout colors
if (shell.FlyoutBackgroundColor is Color flyoutBgColor)
{
platformView.FlyoutBackgroundColor = flyoutBgColor;
}
else if (shell.FlyoutBackground is SolidColorBrush flyoutBrush)
{
platformView.FlyoutBackgroundColor = flyoutBrush.Color;
}
// Sync nav bar colors
if (shell.BackgroundColor is Color bgColor)
{
platformView.NavBarBackgroundColor = bgColor;
}
}
public static void MapFlyoutIsPresented(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler._isUpdatingFlyoutPresented) return;
try
{
handler._isUpdatingFlyoutPresented = true;
handler.PlatformView.FlyoutIsPresented = shell.FlyoutIsPresented;
}
finally
{
handler._isUpdatingFlyoutPresented = false;
}
}
public static void MapFlyoutBehavior(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.FlyoutBehavior = shell.FlyoutBehavior switch
{
Microsoft.Maui.FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
Microsoft.Maui.FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout,
Microsoft.Maui.FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
_ => ShellFlyoutBehavior.Flyout
};
}
public static void MapFlyoutWidth(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.FlyoutWidth = (float)shell.FlyoutWidth;
}
public static void MapFlyoutBackgroundColor(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.FlyoutBackgroundColor is Color color)
{
handler.PlatformView.FlyoutBackgroundColor = color;
}
}
public static void MapFlyoutBackground(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.FlyoutBackground is SolidColorBrush solidBrush)
{
handler.PlatformView.FlyoutBackgroundColor = solidBrush.Color;
}
}
public static void MapBackgroundColor(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.BackgroundColor is Color color)
{
handler.PlatformView.NavBarBackgroundColor = color;
}
}
public static void MapFlyoutHeaderBehavior(ShellHandler handler, Shell shell)
{
// Flyout header behavior - handled by platform view
}
public static void MapFlyoutHeader(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var header = shell.FlyoutHeader;
if (header == null)
{
handler.PlatformView.FlyoutHeaderView = null;
return;
}
if (header is View headerView)
{
if (headerView.Handler == null)
{
headerView.Handler = headerView.ToViewHandler(handler.MauiContext);
}
if (headerView.Handler?.PlatformView is SkiaView skiaHeader)
{
handler.PlatformView.FlyoutHeaderView = skiaHeader;
}
}
}
public static void MapFlyoutFooter(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var footer = shell.FlyoutFooter;
if (footer == null)
{
handler.PlatformView.FlyoutFooterText = null;
return;
}
// Simple text footer support
if (footer is Label label)
{
handler.PlatformView.FlyoutFooterText = label.Text;
}
else if (footer is string text)
{
handler.PlatformView.FlyoutFooterText = text;
}
}
public static void MapItems(ShellHandler handler, Shell shell)
{
handler.SyncShellItems();
}
public static void MapCurrentItem(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
// Sync current item selection
var currentItem = shell.CurrentItem;
if (currentItem != null)
{
// Find matching section index
for (int i = 0; i < handler.PlatformView.Sections.Count; i++)
{
var section = handler.PlatformView.Sections[i];
if (section.Route == (currentItem.Route ?? currentItem.Title))
{
handler.PlatformView.NavigateToSection(i);
break;
}
}
}
}
public static void MapTitle(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Title = shell.Title ?? "";
}
public static void MapGoToAsync(ShellHandler handler, Shell shell, object? args)
{
if (handler.PlatformView is null || args is null) return;
if (args is ShellNavigationState state)
{
handler.PlatformView.GoToAsync(state.Location.ToString());
}
else if (args is string route)
{
handler.PlatformView.GoToAsync(route);
}
} }
} }

View File

@@ -0,0 +1,123 @@
// 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;
/// <summary>
/// Linux handler for Slider control.
/// </summary>
public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
{
public static IPropertyMapper<ISlider, SliderHandler> Mapper = new PropertyMapper<ISlider, SliderHandler>(ViewHandler.ViewMapper)
{
[nameof(ISlider.Minimum)] = MapMinimum,
[nameof(ISlider.Maximum)] = MapMaximum,
[nameof(ISlider.Value)] = MapValue,
[nameof(ISlider.MinimumTrackColor)] = MapMinimumTrackColor,
[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);
public SliderHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSlider CreatePlatformView() => new SkiaSlider();
protected override void ConnectHandler(SkiaSlider platformView)
{
base.ConnectHandler(platformView);
platformView.ValueChanged += OnValueChanged;
platformView.DragStarted += OnDragStarted;
platformView.DragCompleted += OnDragCompleted;
}
protected override void DisconnectHandler(SkiaSlider platformView)
{
platformView.ValueChanged -= OnValueChanged;
platformView.DragStarted -= OnDragStarted;
platformView.DragCompleted -= OnDragCompleted;
base.DisconnectHandler(platformView);
}
private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
{
if (VirtualView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.001)
{
VirtualView.Value = e.NewValue;
}
}
private void OnDragStarted(object? sender, EventArgs e) => VirtualView?.DragStarted();
private void OnDragCompleted(object? sender, EventArgs e) => VirtualView?.DragCompleted();
public static void MapMinimum(SliderHandler handler, ISlider slider)
{
handler.PlatformView.Minimum = slider.Minimum;
handler.PlatformView.Invalidate();
}
public static void MapMaximum(SliderHandler handler, ISlider slider)
{
handler.PlatformView.Maximum = slider.Maximum;
handler.PlatformView.Invalidate();
}
public static void MapValue(SliderHandler handler, ISlider slider)
{
if (Math.Abs(handler.PlatformView.Value - slider.Value) > 0.001)
{
handler.PlatformView.Value = slider.Value;
}
}
public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MinimumTrackColor != null)
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MaximumTrackColor != null)
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapThumbColor(SliderHandler handler, ISlider slider)
{
if (slider.ThumbColor != null)
handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
{
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

@@ -68,7 +68,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnValueChanged(object? sender, ValueChangedEventArgs e) private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
@@ -112,16 +112,18 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// MinimumTrackColor maps to ActiveTrackColor (the filled portion)
if (slider.MinimumTrackColor is not null) if (slider.MinimumTrackColor is not null)
handler.PlatformView.MinimumTrackColor = slider.MinimumTrackColor; handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
} }
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider) public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// MaximumTrackColor maps to TrackColor (the unfilled portion)
if (slider.MaximumTrackColor is not null) if (slider.MaximumTrackColor is not null)
handler.PlatformView.MaximumTrackColor = slider.MaximumTrackColor; handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
} }
public static void MapThumbColor(SliderHandler handler, ISlider slider) public static void MapThumbColor(SliderHandler handler, ISlider slider)
@@ -129,7 +131,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (slider.ThumbColor is not null) if (slider.ThumbColor is not null)
handler.PlatformView.ThumbColor = slider.ThumbColor; handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor();
} }
public static void MapBackground(SliderHandler handler, ISlider slider) public static void MapBackground(SliderHandler handler, ISlider slider)
@@ -138,7 +140,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
if (slider.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (slider.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }

View File

@@ -3,14 +3,13 @@
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary> /// <summary>
/// Handler for Stepper on Linux using Skia rendering. /// Handler for Stepper on Linux using Skia rendering.
/// Maps IStepper interface to SkiaStepper platform view.
/// </summary> /// </summary>
public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper> public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
{ {
@@ -20,9 +19,7 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
[nameof(IStepper.Value)] = MapValue, [nameof(IStepper.Value)] = MapValue,
[nameof(IStepper.Minimum)] = MapMinimum, [nameof(IStepper.Minimum)] = MapMinimum,
[nameof(IStepper.Maximum)] = MapMaximum, [nameof(IStepper.Maximum)] = MapMaximum,
["Increment"] = MapIncrement,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
}; };
public static CommandMapper<IStepper, StepperHandler> CommandMapper = public static CommandMapper<IStepper, StepperHandler> CommandMapper =
@@ -48,26 +45,6 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.ValueChanged += OnValueChanged; platformView.ValueChanged += OnValueChanged;
// Apply dark theme colors if needed
if (Application.Current?.UserAppTheme == AppTheme.Dark)
{
platformView.ButtonBackgroundColor = Color.FromRgb(66, 66, 66);
platformView.ButtonPressedColor = Color.FromRgb(97, 97, 97);
platformView.ButtonDisabledColor = Color.FromRgb(48, 48, 48);
platformView.SymbolColor = Color.FromRgb(224, 224, 224);
platformView.SymbolDisabledColor = Color.FromRgb(97, 97, 97);
platformView.BorderColor = Color.FromRgb(97, 97, 97);
}
// Sync properties
if (VirtualView != null)
{
MapValue(this, VirtualView);
MapMinimum(this, VirtualView);
MapMaximum(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
} }
protected override void DisconnectHandler(SkiaStepper platformView) protected override void DisconnectHandler(SkiaStepper platformView)
@@ -76,21 +53,15 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnValueChanged(object? sender, ValueChangedEventArgs e) private void OnValueChanged(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
VirtualView.Value = PlatformView.Value;
if (Math.Abs(VirtualView.Value - e.NewValue) > 0.0001)
{
VirtualView.Value = e.NewValue;
}
} }
public static void MapValue(StepperHandler handler, IStepper stepper) public static void MapValue(StepperHandler handler, IStepper stepper)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (Math.Abs(handler.PlatformView.Value - stepper.Value) > 0.0001)
handler.PlatformView.Value = stepper.Value; handler.PlatformView.Value = stepper.Value;
} }
@@ -112,24 +83,7 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
public static void MapIncrement(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView is null) return;
if (stepper is Stepper stepperControl)
{
handler.PlatformView.Increment = stepperControl.Increment;
}
}
public static void MapIsEnabled(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = stepper.IsEnabled;
handler.PlatformView.Invalidate();
}
} }

View File

@@ -1,226 +0,0 @@
// 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.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for SwipeView on Linux using Skia rendering.
/// Maps SwipeView to SkiaSwipeView platform view.
/// </summary>
public partial class SwipeViewHandler : ViewHandler<SwipeView, SkiaSwipeView>
{
public static IPropertyMapper<SwipeView, SwipeViewHandler> Mapper =
new PropertyMapper<SwipeView, SwipeViewHandler>(ViewHandler.ViewMapper)
{
[nameof(SwipeView.Content)] = MapContent,
[nameof(SwipeView.LeftItems)] = MapLeftItems,
[nameof(SwipeView.RightItems)] = MapRightItems,
[nameof(SwipeView.TopItems)] = MapTopItems,
[nameof(SwipeView.BottomItems)] = MapBottomItems,
[nameof(SwipeView.Threshold)] = MapThreshold,
[nameof(IView.Background)] = MapBackground,
};
public static CommandMapper<SwipeView, SwipeViewHandler> CommandMapper =
new(ViewHandler.ViewCommandMapper)
{
["RequestOpen"] = MapRequestOpen,
["RequestClose"] = MapRequestClose,
};
public SwipeViewHandler() : base(Mapper, CommandMapper)
{
}
public SwipeViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaSwipeView CreatePlatformView()
{
return new SkiaSwipeView();
}
protected override void ConnectHandler(SkiaSwipeView platformView)
{
base.ConnectHandler(platformView);
platformView.SwipeStarted += OnSwipeStarted;
platformView.SwipeEnded += OnSwipeEnded;
}
protected override void DisconnectHandler(SkiaSwipeView platformView)
{
platformView.SwipeStarted -= OnSwipeStarted;
platformView.SwipeEnded -= OnSwipeEnded;
base.DisconnectHandler(platformView);
}
private void OnSwipeStarted(object? sender, Platform.SwipeStartedEventArgs e)
{
// SwipeView events are handled internally by the platform view
}
private void OnSwipeEnded(object? sender, Platform.SwipeEndedEventArgs e)
{
// SwipeView events are handled internally by the platform view
}
public static void MapContent(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var content = swipeView.Content;
if (content == null)
{
handler.PlatformView.Content = null;
return;
}
// Create handler for content
if (content.Handler == null)
{
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
handler.PlatformView.Content = skiaContent;
}
}
public static void MapLeftItems(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.LeftItems.Clear();
if (swipeView.LeftItems != null)
{
foreach (var item in swipeView.LeftItems)
{
handler.PlatformView.LeftItems.Add(CreatePlatformSwipeItem(item));
}
}
}
public static void MapRightItems(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.RightItems.Clear();
if (swipeView.RightItems != null)
{
foreach (var item in swipeView.RightItems)
{
handler.PlatformView.RightItems.Add(CreatePlatformSwipeItem(item));
}
}
}
public static void MapTopItems(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.TopItems.Clear();
if (swipeView.TopItems != null)
{
foreach (var item in swipeView.TopItems)
{
handler.PlatformView.TopItems.Add(CreatePlatformSwipeItem(item));
}
}
}
public static void MapBottomItems(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.BottomItems.Clear();
if (swipeView.BottomItems != null)
{
foreach (var item in swipeView.BottomItems)
{
handler.PlatformView.BottomItems.Add(CreatePlatformSwipeItem(item));
}
}
}
public static void MapThreshold(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.LeftSwipeThreshold = (float)swipeView.Threshold;
handler.PlatformView.RightSwipeThreshold = (float)swipeView.Threshold;
}
public static void MapBackground(SwipeViewHandler handler, SwipeView swipeView)
{
if (handler.PlatformView is null) return;
if (swipeView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
public static void MapRequestOpen(SwipeViewHandler handler, SwipeView swipeView, object? args)
{
if (handler.PlatformView is null) return;
if (args is SwipeViewOpenRequest request)
{
var direction = request.OpenSwipeItem switch
{
OpenSwipeItem.LeftItems => Platform.SwipeDirection.Right,
OpenSwipeItem.RightItems => Platform.SwipeDirection.Left,
OpenSwipeItem.TopItems => Platform.SwipeDirection.Down,
OpenSwipeItem.BottomItems => Platform.SwipeDirection.Up,
_ => Platform.SwipeDirection.Right
};
handler.PlatformView.Open(direction);
}
}
public static void MapRequestClose(SwipeViewHandler handler, SwipeView swipeView, object? args)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Close();
}
private static Platform.SwipeItem CreatePlatformSwipeItem(ISwipeItem item)
{
var platformItem = new Platform.SwipeItem();
if (item is Controls.SwipeItem swipeItem)
{
platformItem.Text = swipeItem.Text ?? "";
// Get background color
var bgColor = swipeItem.BackgroundColor;
if (bgColor is not null)
{
platformItem.BackgroundColor = bgColor;
}
}
else if (item is Controls.SwipeItemView swipeItemView)
{
// SwipeItemView uses custom content - use a simple representation
platformItem.Text = "Action";
platformItem.BackgroundColor = Color.FromRgb(100, 100, 100);
}
return platformItem;
}
}

View File

@@ -0,0 +1,94 @@
// 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;
/// <summary>
/// Linux handler for Switch control.
/// </summary>
public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
{
public static IPropertyMapper<ISwitch, SwitchHandler> Mapper = new PropertyMapper<ISwitch, SwitchHandler>(ViewHandler.ViewMapper)
{
[nameof(ISwitch.IsOn)] = MapIsOn,
[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);
public SwitchHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSwitch CreatePlatformView() => new SkiaSwitch();
protected override void ConnectHandler(SkiaSwitch platformView)
{
base.ConnectHandler(platformView);
platformView.Toggled += OnToggled;
}
protected override void DisconnectHandler(SkiaSwitch platformView)
{
platformView.Toggled -= OnToggled;
base.DisconnectHandler(platformView);
}
private void OnToggled(object? sender, ToggledEventArgs e)
{
if (VirtualView != null && VirtualView.IsOn != e.Value)
{
VirtualView.IsOn = e.Value;
}
}
public static void MapIsOn(SwitchHandler handler, ISwitch @switch)
{
if (handler.PlatformView.IsOn != @switch.IsOn)
{
handler.PlatformView.IsOn = @switch.IsOn;
}
}
public static void MapTrackColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.TrackColor != null)
handler.PlatformView.OnTrackColor = @switch.TrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapThumbColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.ThumbColor != null)
handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
{
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

@@ -19,7 +19,6 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
[nameof(ISwitch.TrackColor)] = MapTrackColor, [nameof(ISwitch.TrackColor)] = MapTrackColor,
[nameof(ISwitch.ThumbColor)] = MapThumbColor, [nameof(ISwitch.ThumbColor)] = MapThumbColor,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
}; };
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -70,12 +69,13 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// TrackColor sets the On track color (MAUI's OnColor) // TrackColor sets both On and Off track colors
if (@switch.TrackColor is not null) if (@switch.TrackColor is not null)
{ {
handler.PlatformView.OnTrackColor = @switch.TrackColor; var color = @switch.TrackColor.ToSKColor();
// Off track is a lighter/desaturated version handler.PlatformView.OnTrackColor = color;
handler.PlatformView.OffTrackColor = @switch.TrackColor.WithAlpha(0.5f); // Off track could be a lighter version
handler.PlatformView.OffTrackColor = color.WithAlpha(128);
} }
} }
@@ -84,7 +84,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (@switch.ThumbColor is not null) if (@switch.ThumbColor is not null)
handler.PlatformView.ThumbColor = @switch.ThumbColor; handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor();
} }
public static void MapBackground(SwitchHandler handler, ISwitch @switch) public static void MapBackground(SwitchHandler handler, ISwitch @switch)
@@ -93,14 +93,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
// Background color for the switch container (not the track) handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidPaint.Color;
} }
} }
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = @switch.IsEnabled;
}
} }

View File

@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -15,14 +13,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary> /// </summary>
public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage> public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage>
{ {
private bool _isUpdatingSelection;
public static IPropertyMapper<ITabbedView, TabbedPageHandler> Mapper = new PropertyMapper<ITabbedView, TabbedPageHandler>(ViewHandler.ViewMapper) public static IPropertyMapper<ITabbedView, TabbedPageHandler> Mapper = new PropertyMapper<ITabbedView, TabbedPageHandler>(ViewHandler.ViewMapper)
{ {
[nameof(TabbedPage.BarBackgroundColor)] = MapBarBackgroundColor,
[nameof(TabbedPage.BarTextColor)] = MapBarTextColor,
[nameof(TabbedPage.SelectedTabColor)] = MapSelectedTabColor,
[nameof(TabbedPage.UnselectedTabColor)] = MapUnselectedTabColor,
}; };
public static CommandMapper<ITabbedView, TabbedPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<ITabbedView, TabbedPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -47,9 +39,6 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.SelectedIndexChanged += OnSelectedIndexChanged; platformView.SelectedIndexChanged += OnSelectedIndexChanged;
// Sync initial tabs
SyncTabs();
} }
protected override void DisconnectHandler(SkiaTabbedPage platformView) protected override void DisconnectHandler(SkiaTabbedPage platformView)
@@ -61,104 +50,6 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
private void OnSelectedIndexChanged(object? sender, EventArgs e) private void OnSelectedIndexChanged(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null || _isUpdatingSelection) return; // Notify the virtual view of selection change
try
{
_isUpdatingSelection = true;
// Sync selected page back to virtual view
if (VirtualView is TabbedPage tabbedPage && PlatformView.SelectedIndex >= 0)
{
var selectedIndex = PlatformView.SelectedIndex;
if (selectedIndex < tabbedPage.Children.Count)
{
tabbedPage.CurrentPage = tabbedPage.Children[selectedIndex] as Page;
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
private void SyncTabs()
{
if (PlatformView is null || VirtualView is null || MauiContext is null) return;
PlatformView.ClearTabs();
if (VirtualView is TabbedPage tabbedPage)
{
foreach (var child in tabbedPage.Children)
{
if (child is Page page)
{
// Create handler for page content
if (page.Handler == null)
{
page.Handler = page.ToViewHandler(MauiContext);
}
if (page.Handler?.PlatformView is SkiaView skiaContent)
{
PlatformView.AddTab(page.Title ?? "Tab", skiaContent, page.IconImageSource?.ToString());
}
}
}
// Sync selected tab
if (tabbedPage.CurrentPage != null)
{
var index = tabbedPage.Children.IndexOf(tabbedPage.CurrentPage);
if (index >= 0)
{
PlatformView.SelectedIndex = index;
}
}
}
}
public static void MapBarBackgroundColor(TabbedPageHandler handler, ITabbedView tabbedView)
{
if (handler.PlatformView is null) return;
if (tabbedView is TabbedPage tabbedPage && tabbedPage.BarBackgroundColor is Color color)
{
handler.PlatformView.TabBarBackgroundColor = color;
}
}
public static void MapBarTextColor(TabbedPageHandler handler, ITabbedView tabbedView)
{
if (handler.PlatformView is null) return;
if (tabbedView is TabbedPage tabbedPage && tabbedPage.BarTextColor is Color color)
{
// BarTextColor applies to unselected tabs
handler.PlatformView.UnselectedTabColor = color;
}
}
public static void MapSelectedTabColor(TabbedPageHandler handler, ITabbedView tabbedView)
{
if (handler.PlatformView is null) return;
if (tabbedView is TabbedPage tabbedPage && tabbedPage.SelectedTabColor is Color color)
{
handler.PlatformView.SelectedTabColor = color;
handler.PlatformView.IndicatorColor = color;
}
}
public static void MapUnselectedTabColor(TabbedPageHandler handler, ITabbedView tabbedView)
{
if (handler.PlatformView is null) return;
if (tabbedView is TabbedPage tabbedPage && tabbedPage.UnselectedTabColor is Color color)
{
handler.PlatformView.UnselectedTabColor = color;
}
} }
} }

View File

@@ -21,7 +21,6 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
[nameof(ITimePicker.Format)] = MapFormat, [nameof(ITimePicker.Format)] = MapFormat,
[nameof(ITimePicker.TextColor)] = MapTextColor, [nameof(ITimePicker.TextColor)] = MapTextColor,
[nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing, [nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
}; };
@@ -48,16 +47,6 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.TimeSelected += OnTimeSelected; platformView.TimeSelected += OnTimeSelected;
// Apply dark theme colors if needed
if (Application.Current?.UserAppTheme == AppTheme.Dark)
{
platformView.ClockBackgroundColor = Color.FromRgb(30, 30, 30);
platformView.ClockFaceColor = Color.FromRgb(45, 45, 45);
platformView.TextColor = Color.FromRgb(224, 224, 224);
platformView.BorderColor = Color.FromRgb(97, 97, 97);
platformView.BackgroundColor = Color.FromRgb(45, 45, 45);
}
} }
protected override void DisconnectHandler(SkiaTimePicker platformView) protected override void DisconnectHandler(SkiaTimePicker platformView)
@@ -66,11 +55,11 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnTimeSelected(object? sender, TimeChangedEventArgs e) private void OnTimeSelected(object? sender, EventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
VirtualView.Time = e.NewTime; VirtualView.Time = PlatformView.Time;
} }
public static void MapTime(TimePickerHandler handler, ITimePicker timePicker) public static void MapTime(TimePickerHandler handler, ITimePicker timePicker)
@@ -90,32 +79,13 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (timePicker.TextColor is not null) if (timePicker.TextColor is not null)
{ {
handler.PlatformView.TextColor = timePicker.TextColor; handler.PlatformView.TextColor = timePicker.TextColor.ToSKColor();
} }
} }
public static void MapCharacterSpacing(TimePickerHandler handler, ITimePicker timePicker) public static void MapCharacterSpacing(TimePickerHandler handler, ITimePicker timePicker)
{ {
if (handler.PlatformView is null) return; // Character spacing would require custom text rendering
handler.PlatformView.CharacterSpacing = timePicker.CharacterSpacing;
}
public static void MapFont(TimePickerHandler handler, ITimePicker timePicker)
{
if (handler.PlatformView is null) return;
var font = timePicker.Font;
if (font.Size > 0)
handler.PlatformView.FontSize = font.Size;
if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family;
// Map FontAttributes from the Font weight/slant
var attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
handler.PlatformView.FontAttributes = attrs;
} }
public static void MapBackground(TimePickerHandler handler, ITimePicker timePicker) public static void MapBackground(TimePickerHandler handler, ITimePicker timePicker)
@@ -124,7 +94,7 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
if (timePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (timePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color; handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }
} }

View File

@@ -1,208 +0,0 @@
// 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.Platform.Linux.Services;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for WebView control using WebKitGTK.
/// </summary>
public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
{
/// <summary>
/// Property mapper for WebView properties.
/// </summary>
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IWebView.Source)] = MapSource,
[nameof(IWebView.UserAgent)] = MapUserAgent,
};
/// <summary>
/// Command mapper for WebView commands.
/// </summary>
public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
[nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload,
[nameof(IWebView.Eval)] = MapEval,
[nameof(IWebView.EvaluateJavaScriptAsync)] = MapEvaluateJavaScriptAsync,
};
public WebViewHandler() : base(Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override LinuxWebView CreatePlatformView()
{
DiagnosticLog.Debug("WebViewHandler", "Creating LinuxWebView");
return new LinuxWebView();
}
protected override void ConnectHandler(LinuxWebView platformView)
{
base.ConnectHandler(platformView);
platformView.Navigating += OnNavigating;
platformView.Navigated += OnNavigated;
// Map initial properties
if (VirtualView != null)
{
MapSource(this, VirtualView);
MapUserAgent(this, VirtualView);
}
DiagnosticLog.Debug("WebViewHandler", "Handler connected");
}
protected override void DisconnectHandler(LinuxWebView platformView)
{
platformView.Navigating -= OnNavigating;
platformView.Navigated -= OnNavigated;
base.DisconnectHandler(platformView);
DiagnosticLog.Debug("WebViewHandler", "Handler disconnected");
}
private void OnNavigating(object? sender, WebViewNavigatingEventArgs e)
{
if (VirtualView == null)
return;
// Notify the virtual view about navigation starting
VirtualView.Navigating(WebNavigationEvent.NewPage, e.Url);
}
private void OnNavigated(object? sender, WebViewNavigatedEventArgs e)
{
if (VirtualView == null)
return;
// Notify the virtual view about navigation completed
var result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
VirtualView.Navigated(WebNavigationEvent.NewPage, e.Url, result);
}
#region Property Mappers
public static void MapSource(WebViewHandler handler, IWebView webView)
{
var source = webView.Source;
if (source == null)
return;
DiagnosticLog.Debug("WebViewHandler", $"MapSource: {source.GetType().Name}");
if (source is IUrlWebViewSource urlSource && !string.IsNullOrEmpty(urlSource.Url))
{
handler.PlatformView?.LoadUrl(urlSource.Url);
}
else if (source is IHtmlWebViewSource htmlSource && !string.IsNullOrEmpty(htmlSource.Html))
{
handler.PlatformView?.LoadHtml(htmlSource.Html, htmlSource.BaseUrl);
}
}
public static void MapUserAgent(WebViewHandler handler, IWebView webView)
{
if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent))
{
handler.PlatformView.UserAgent = webView.UserAgent;
DiagnosticLog.Debug("WebViewHandler", $"MapUserAgent: {webView.UserAgent}");
}
}
#endregion
#region Command Mappers
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
{
if (handler.PlatformView?.CanGoBack == true)
{
handler.PlatformView.GoBack();
DiagnosticLog.Debug("WebViewHandler", "GoBack");
}
}
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
{
if (handler.PlatformView?.CanGoForward == true)
{
handler.PlatformView.GoForward();
DiagnosticLog.Debug("WebViewHandler", "GoForward");
}
}
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.Reload();
DiagnosticLog.Debug("WebViewHandler", "Reload");
}
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
{
if (args is string script)
{
handler.PlatformView?.Eval(script);
DiagnosticLog.Debug("WebViewHandler", $"Eval: {script.Substring(0, Math.Min(50, script.Length))}...");
}
}
public static void MapEvaluateJavaScriptAsync(WebViewHandler handler, IWebView webView, object? args)
{
if (args is EvaluateJavaScriptAsyncRequest request)
{
var result = handler.PlatformView?.EvaluateJavaScriptAsync(request.Script);
if (result != null)
{
result.ContinueWith(t =>
{
request.SetResult(t.Result);
});
}
else
{
request.SetResult(null);
}
DiagnosticLog.Debug("WebViewHandler", $"EvaluateJavaScriptAsync: {request.Script.Substring(0, Math.Min(50, request.Script.Length))}...");
}
}
#endregion
}
/// <summary>
/// Request object for async JavaScript evaluation.
/// </summary>
public class EvaluateJavaScriptAsyncRequest
{
public string Script { get; }
private readonly TaskCompletionSource<string?> _tcs = new();
public EvaluateJavaScriptAsyncRequest(string script)
{
Script = script;
}
public Task<string?> Task => _tcs.Task;
public void SetResult(string? result)
{
_tcs.TrySetResult(result);
}
}

View File

@@ -4,7 +4,6 @@
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -16,7 +15,6 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper) public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
{ {
[nameof(IWebView.Source)] = MapSource, [nameof(IWebView.Source)] = MapSource,
[nameof(IWebView.UserAgent)] = MapUserAgent,
}; };
public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -24,8 +22,6 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
[nameof(IWebView.GoBack)] = MapGoBack, [nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward, [nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload, [nameof(IWebView.Reload)] = MapReload,
[nameof(IWebView.Eval)] = MapEval,
[nameof(IWebView.EvaluateJavaScriptAsync)] = MapEvaluateJavaScriptAsync,
}; };
public WebViewHandler() : base(Mapper, CommandMapper) public WebViewHandler() : base(Mapper, CommandMapper)
@@ -58,63 +54,29 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnNavigating(object? sender, Microsoft.Maui.Platform.WebNavigatingEventArgs e) private void OnNavigating(object? sender, WebNavigatingEventArgs e)
{ {
IWebView virtualView = VirtualView; // Forward to virtual view if needed
IWebViewController? controller = virtualView as IWebViewController;
if (controller != null)
{
var args = new Microsoft.Maui.Controls.WebNavigatingEventArgs(
WebNavigationEvent.NewPage,
null,
e.Url);
controller.SendNavigating(args);
}
} }
private void OnNavigated(object? sender, Microsoft.Maui.Platform.WebNavigatedEventArgs e) private void OnNavigated(object? sender, WebNavigatedEventArgs e)
{ {
IWebView virtualView = VirtualView; // Forward to virtual view if needed
IWebViewController? controller = virtualView as IWebViewController;
if (controller != null)
{
WebNavigationResult result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
var args = new Microsoft.Maui.Controls.WebNavigatedEventArgs(
WebNavigationEvent.NewPage,
null,
e.Url,
result);
controller.SendNavigated(args);
}
} }
public static void MapSource(WebViewHandler handler, IWebView webView) public static void MapSource(WebViewHandler handler, IWebView webView)
{ {
DiagnosticLog.Debug("WebViewHandler", "MapSource called"); if (handler.PlatformView == null) return;
if (handler.PlatformView == null)
{
DiagnosticLog.Warn("WebViewHandler", "PlatformView is null!");
return;
}
var source = webView.Source; var source = webView.Source;
DiagnosticLog.Debug("WebViewHandler", $"Source type: {source?.GetType().Name ?? "null"}");
if (source is UrlWebViewSource urlSource) if (source is UrlWebViewSource urlSource)
{ {
DiagnosticLog.Debug("WebViewHandler", $"Loading URL: {urlSource.Url}");
handler.PlatformView.Source = urlSource.Url ?? ""; handler.PlatformView.Source = urlSource.Url ?? "";
} }
else if (source is HtmlWebViewSource htmlSource) else if (source is HtmlWebViewSource htmlSource)
{ {
DiagnosticLog.Debug("WebViewHandler", $"Loading HTML ({htmlSource.Html?.Length ?? 0} chars)");
DiagnosticLog.Debug("WebViewHandler", $"HTML preview: {htmlSource.Html?.Substring(0, Math.Min(100, htmlSource.Html?.Length ?? 0))}...");
handler.PlatformView.Html = htmlSource.Html ?? ""; handler.PlatformView.Html = htmlSource.Html ?? "";
} }
else
{
DiagnosticLog.Debug("WebViewHandler", "Unknown source type or null");
}
} }
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args) public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
@@ -131,66 +93,4 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
{ {
handler.PlatformView?.Reload(); handler.PlatformView?.Reload();
} }
public static void MapUserAgent(WebViewHandler handler, IWebView webView)
{
if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent))
{
handler.PlatformView.UserAgent = webView.UserAgent;
}
}
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
{
if (args is string script)
{
handler.PlatformView?.Eval(script);
}
}
public static void MapEvaluateJavaScriptAsync(WebViewHandler handler, IWebView webView, object? args)
{
// Handle EvaluateJavaScriptAsyncRequest from Microsoft.Maui.Platform namespace
if (args is EvaluateJavaScriptAsyncRequest request)
{
var result = handler.PlatformView?.EvaluateJavaScriptAsync(request.Script);
if (result != null)
{
result.ContinueWith(t =>
{
request.SetResult(t.Result);
});
}
else
{
request.SetResult(null);
}
}
else if (args is string script)
{
// Direct script string
handler.PlatformView?.EvaluateJavaScriptAsync(script);
}
}
}
/// <summary>
/// Request object for async JavaScript evaluation (matches Microsoft.Maui.Platform.EvaluateJavaScriptAsyncRequest).
/// </summary>
public class EvaluateJavaScriptAsyncRequest
{
public string Script { get; }
private readonly System.Threading.Tasks.TaskCompletionSource<string?> _tcs = new();
public EvaluateJavaScriptAsyncRequest(string script)
{
Script = script;
}
public System.Threading.Tasks.Task<string?> Task => _tcs.Task;
public void SetResult(string? result)
{
_tcs.TrySetResult(result);
}
} }

View File

@@ -5,7 +5,6 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -82,20 +81,13 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
public static void MapContent(WindowHandler handler, IWindow window) public static void MapContent(WindowHandler handler, IWindow window)
{ {
DiagnosticLog.Debug("WindowHandler", $"MapContent - PlatformView={handler.PlatformView != null}");
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
var content = window.Content; var content = window.Content;
DiagnosticLog.Debug("WindowHandler", $"MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
if (content?.Handler?.PlatformView is SkiaView skiaContent) if (content?.Handler?.PlatformView is SkiaView skiaContent)
{ {
DiagnosticLog.Debug("WindowHandler", $"MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent; handler.PlatformView.Content = skiaContent;
} }
else
{
DiagnosticLog.Warn("WindowHandler", $"MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
}
} }
public static void MapX(WindowHandler handler, IWindow window) public static void MapX(WindowHandler handler, IWindow window)
@@ -185,8 +177,8 @@ public class SkiaWindow
// Draw main content // Draw main content
if (_content != null) if (_content != null)
{ {
_content.Measure(new Size(_width, _height)); _content.Measure(new SKSize(_width, _height));
_content.Arrange(new Rect(0, 0, _width, _height)); _content.Arrange(new SKRect(0, 0, _width, _height));
_content.Draw(canvas); _content.Draw(canvas);
} }

View File

@@ -1,52 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform.Linux.Dispatching;
namespace Microsoft.Maui.Platform.Linux.Hosting;
public class GtkMauiContext : IMauiContext
{
private readonly IServiceProvider _services;
private readonly IMauiHandlersFactory _handlers;
private IAnimationManager? _animationManager;
private IDispatcher? _dispatcher;
public IServiceProvider Services => _services;
public IMauiHandlersFactory Handlers => _handlers;
public IAnimationManager AnimationManager
{
get
{
_animationManager ??= _services.GetService<IAnimationManager>()
?? new LinuxAnimationManager(new LinuxTicker());
return _animationManager;
}
}
public IDispatcher Dispatcher
{
get
{
_dispatcher ??= _services.GetService<IDispatcher>()
?? new LinuxDispatcher();
return _dispatcher;
}
}
public GtkMauiContext(IServiceProvider services)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_handlers = services.GetRequiredService<IMauiHandlersFactory>();
if (LinuxApplication.Current == null)
{
new LinuxApplication();
}
}
}

View File

@@ -1,17 +0,0 @@
// 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.Hosting;
namespace Microsoft.Maui.Platform.Linux.Hosting;
public static class HandlerMappingExtensions
{
public static IMauiHandlersCollection AddHandler<TView, THandler>(this IMauiHandlersCollection handlers)
where TView : class
where THandler : class
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
}
}

View File

@@ -1,56 +0,0 @@
// 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.Animations;
using Animation = Microsoft.Maui.Animations.Animation;
namespace Microsoft.Maui.Platform.Linux.Hosting;
internal class LinuxAnimationManager : IAnimationManager
{
private readonly List<Animation> _animations = new();
private readonly ITicker _ticker;
public double SpeedModifier { get; set; } = 1.0;
public bool AutoStartTicker { get; set; } = true;
public ITicker Ticker => _ticker;
public LinuxAnimationManager(ITicker ticker)
{
_ticker = ticker;
_ticker.Fire = OnTickerFire;
}
public void Add(Animation animation)
{
_animations.Add(animation);
if (AutoStartTicker && !_ticker.IsRunning)
{
_ticker.Start();
}
}
public void Remove(Animation animation)
{
_animations.Remove(animation);
if (_animations.Count == 0 && _ticker.IsRunning)
{
_ticker.Stop();
}
}
private void OnTickerFire()
{
var animationsArray = _animations.ToArray();
foreach (var animation in animationsArray)
{
animation.Tick(0.016 * SpeedModifier);
if (animation.HasFinished)
{
Remove(animation);
}
}
}
}

View File

@@ -7,41 +7,37 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.ApplicationModel.Communication; using Microsoft.Maui.ApplicationModel.Communication;
using Microsoft.Maui.ApplicationModel.DataTransfer; using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Hosting; using Microsoft.Maui.Hosting;
using Microsoft.Maui.Networking;
using Microsoft.Maui.Platform.Linux.Converters;
using Microsoft.Maui.Platform.Linux.Dispatching;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Converters;
using Microsoft.Maui.Storage; using Microsoft.Maui.Storage;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Controls;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for configuring MAUI applications for Linux.
/// </summary>
public static class LinuxMauiAppBuilderExtensions public static class LinuxMauiAppBuilderExtensions
{ {
/// <summary>
/// Configures the MAUI application to run on Linux.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder) public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
{ {
return builder.UseLinux(null); return builder.UseLinux(configure: null);
} }
/// <summary>
/// Configures the MAUI application to run on Linux with options.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure) public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure)
{ {
var options = new LinuxApplicationOptions(); var options = new LinuxApplicationOptions();
configure?.Invoke(options); configure?.Invoke(options);
// Register dispatcher provider
builder.Services.TryAddSingleton<IDispatcherProvider>(LinuxDispatcherProvider.Instance);
// Register device services
builder.Services.TryAddSingleton<IDeviceInfo>(DeviceInfoService.Instance);
builder.Services.TryAddSingleton<IDeviceDisplay>(DeviceDisplayService.Instance);
builder.Services.TryAddSingleton<IAppInfo>(AppInfoService.Instance);
builder.Services.TryAddSingleton<IConnectivity>(ConnectivityService.Instance);
// Register platform services // Register platform services
builder.Services.TryAddSingleton<ILauncher, LauncherService>(); builder.Services.TryAddSingleton<ILauncher, LauncherService>();
builder.Services.TryAddSingleton<IPreferences, PreferencesService>(); builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
@@ -54,29 +50,6 @@ public static class LinuxMauiAppBuilderExtensions
builder.Services.TryAddSingleton<IBrowser, BrowserService>(); builder.Services.TryAddSingleton<IBrowser, BrowserService>();
builder.Services.TryAddSingleton<IEmail, EmailService>(); builder.Services.TryAddSingleton<IEmail, EmailService>();
// Register theming and accessibility services
builder.Services.TryAddSingleton<SystemThemeService>();
builder.Services.TryAddSingleton<HighContrastService>();
// Register accessibility service
builder.Services.TryAddSingleton<IAccessibilityService>(_ => AccessibilityServiceFactory.Instance);
// Register input method service
builder.Services.TryAddSingleton<IInputMethodService>(_ => InputMethodServiceFactory.Instance);
// Register font fallback manager
builder.Services.TryAddSingleton(_ => FontFallbackManager.Instance);
// Register additional Linux-specific services
builder.Services.TryAddSingleton<FolderPickerService>();
builder.Services.TryAddSingleton<NotificationService>();
builder.Services.TryAddSingleton<SystemTrayService>();
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
builder.Services.TryAddSingleton<DragDropService>();
// Register GTK host service
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
// Register type converters for XAML support // Register type converters for XAML support
RegisterTypeConverters(); RegisterTypeConverters();
@@ -104,12 +77,11 @@ public static class LinuxMauiAppBuilderExtensions
handlers.AddHandler<VerticalStackLayout, StackLayoutHandler>(); handlers.AddHandler<VerticalStackLayout, StackLayoutHandler>();
handlers.AddHandler<HorizontalStackLayout, StackLayoutHandler>(); handlers.AddHandler<HorizontalStackLayout, StackLayoutHandler>();
handlers.AddHandler<AbsoluteLayout, LayoutHandler>(); handlers.AddHandler<AbsoluteLayout, LayoutHandler>();
handlers.AddHandler<FlexLayout, FlexLayoutHandler>(); handlers.AddHandler<FlexLayout, LayoutHandler>();
handlers.AddHandler<ScrollView, ScrollViewHandler>(); handlers.AddHandler<ScrollView, ScrollViewHandler>();
handlers.AddHandler<Frame, FrameHandler>(); handlers.AddHandler<Frame, FrameHandler>();
handlers.AddHandler<Border, BorderHandler>(); handlers.AddHandler<Border, BorderHandler>();
handlers.AddHandler<ContentView, BorderHandler>(); handlers.AddHandler<ContentView, BorderHandler>();
handlers.AddHandler<RefreshView, RefreshViewHandler>();
// Picker controls // Picker controls
handlers.AddHandler<Picker, PickerHandler>(); handlers.AddHandler<Picker, PickerHandler>();
@@ -126,15 +98,9 @@ public static class LinuxMauiAppBuilderExtensions
handlers.AddHandler<ImageButton, ImageButtonHandler>(); handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<GraphicsView, GraphicsViewHandler>(); handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
// Web - use GtkWebViewHandler
handlers.AddHandler<WebView, GtkWebViewHandler>();
// Collection Views // Collection Views
handlers.AddHandler<CollectionView, CollectionViewHandler>(); handlers.AddHandler<CollectionView, CollectionViewHandler>();
handlers.AddHandler<ListView, CollectionViewHandler>(); handlers.AddHandler<ListView, CollectionViewHandler>();
handlers.AddHandler<CarouselView, CarouselViewHandler>();
handlers.AddHandler<IndicatorView, IndicatorViewHandler>();
handlers.AddHandler<SwipeView, SwipeViewHandler>();
// Pages & Navigation // Pages & Navigation
handlers.AddHandler<Page, PageHandler>(); handlers.AddHandler<Page, PageHandler>();
@@ -155,11 +121,33 @@ public static class LinuxMauiAppBuilderExtensions
return builder; return builder;
} }
/// <summary>
/// Registers custom type converters for Linux platform.
/// </summary>
private static void RegisterTypeConverters() private static void RegisterTypeConverters()
{ {
// Register SkiaSharp type converters for XAML styling support
TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter))); TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter))); TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter))); TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter))); TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter)));
} }
} }
/// <summary>
/// Handler registration extensions.
/// </summary>
public static class HandlerMappingExtensions
{
/// <summary>
/// Adds a handler for the specified view type.
/// </summary>
public static IMauiHandlersCollection AddHandler<TView, THandler>(
this IMauiHandlersCollection handlers)
where TView : class
where THandler : class
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
}
}

View File

@@ -4,10 +4,15 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Animations; using Microsoft.Maui.Animations;
using Microsoft.Maui.Dispatching; using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform.Linux.Dispatching; using Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Linux-specific implementation of IMauiContext.
/// Provides the infrastructure for creating handlers and accessing platform services.
/// </summary>
public class LinuxMauiContext : IMauiContext public class LinuxMauiContext : IMauiContext
{ {
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
@@ -16,12 +21,27 @@ public class LinuxMauiContext : IMauiContext
private IAnimationManager? _animationManager; private IAnimationManager? _animationManager;
private IDispatcher? _dispatcher; private IDispatcher? _dispatcher;
public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp));
_handlers = services.GetRequiredService<IMauiHandlersFactory>();
}
/// <inheritdoc />
public IServiceProvider Services => _services; public IServiceProvider Services => _services;
/// <inheritdoc />
public IMauiHandlersFactory Handlers => _handlers; public IMauiHandlersFactory Handlers => _handlers;
/// <summary>
/// Gets the Linux application instance.
/// </summary>
public LinuxApplication LinuxApp => _linuxApp; public LinuxApplication LinuxApp => _linuxApp;
/// <summary>
/// Gets the animation manager.
/// </summary>
public IAnimationManager AnimationManager public IAnimationManager AnimationManager
{ {
get get
@@ -32,6 +52,9 @@ public class LinuxMauiContext : IMauiContext
} }
} }
/// <summary>
/// Gets the dispatcher for UI thread operations.
/// </summary>
public IDispatcher Dispatcher public IDispatcher Dispatcher
{ {
get get
@@ -41,11 +64,236 @@ public class LinuxMauiContext : IMauiContext
return _dispatcher; return _dispatcher;
} }
} }
}
public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp) /// <summary>
/// Scoped MAUI context for a specific window or view hierarchy.
/// </summary>
public class ScopedLinuxMauiContext : IMauiContext
{ {
_services = services ?? throw new ArgumentNullException(nameof(services)); private readonly LinuxMauiContext _parent;
_linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp));
_handlers = services.GetRequiredService<IMauiHandlersFactory>(); public ScopedLinuxMauiContext(LinuxMauiContext parent)
{
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
}
public IServiceProvider Services => _parent.Services;
public IMauiHandlersFactory Handlers => _parent.Handlers;
}
/// <summary>
/// Linux dispatcher for UI thread operations.
/// </summary>
internal class LinuxDispatcher : IDispatcher
{
private readonly object _lock = new();
private readonly Queue<Action> _queue = new();
private bool _isDispatching;
public bool IsDispatchRequired => false; // Linux uses single-threaded event loop
public IDispatcherTimer CreateTimer()
{
return new LinuxDispatcherTimer();
}
public bool Dispatch(Action action)
{
if (action == null)
return false;
lock (_lock)
{
_queue.Enqueue(action);
}
ProcessQueue();
return true;
}
public bool DispatchDelayed(TimeSpan delay, Action action)
{
if (action == null)
return false;
Task.Delay(delay).ContinueWith(_ => Dispatch(action));
return true;
}
private void ProcessQueue()
{
if (_isDispatching)
return;
_isDispatching = true;
try
{
while (true)
{
Action? action;
lock (_lock)
{
if (_queue.Count == 0)
break;
action = _queue.Dequeue();
}
action?.Invoke();
}
}
finally
{
_isDispatching = false;
}
}
}
/// <summary>
/// Linux dispatcher timer implementation.
/// </summary>
internal class LinuxDispatcherTimer : IDispatcherTimer
{
private Timer? _timer;
private TimeSpan _interval = TimeSpan.FromMilliseconds(16); // ~60fps default
private bool _isRunning;
private bool _isRepeating = true;
public TimeSpan Interval
{
get => _interval;
set => _interval = value;
}
public bool IsRunning => _isRunning;
public bool IsRepeating
{
get => _isRepeating;
set => _isRepeating = value;
}
public event EventHandler? Tick;
public void Start()
{
if (_isRunning)
return;
_isRunning = true;
_timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan);
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Tick?.Invoke(this, EventArgs.Empty);
if (!_isRepeating)
{
Stop();
}
}
}
/// <summary>
/// Linux animation manager.
/// </summary>
internal class LinuxAnimationManager : IAnimationManager
{
private readonly List<Microsoft.Maui.Animations.Animation> _animations = new();
private readonly ITicker _ticker;
public LinuxAnimationManager(ITicker ticker)
{
_ticker = ticker;
_ticker.Fire = OnTickerFire;
}
public double SpeedModifier { get; set; } = 1.0;
public bool AutoStartTicker { get; set; } = true;
public ITicker Ticker => _ticker;
public void Add(Microsoft.Maui.Animations.Animation animation)
{
_animations.Add(animation);
if (AutoStartTicker && !_ticker.IsRunning)
{
_ticker.Start();
}
}
public void Remove(Microsoft.Maui.Animations.Animation animation)
{
_animations.Remove(animation);
if (_animations.Count == 0 && _ticker.IsRunning)
{
_ticker.Stop();
}
}
private void OnTickerFire()
{
var animations = _animations.ToArray();
foreach (var animation in animations)
{
animation.Tick(16.0 / 1000.0 * SpeedModifier); // ~60fps
if (animation.HasFinished)
{
Remove(animation);
}
}
}
}
/// <summary>
/// Linux ticker for animation timing.
/// </summary>
internal class LinuxTicker : ITicker
{
private Timer? _timer;
private bool _isRunning;
private int _maxFps = 60;
public bool IsRunning => _isRunning;
public bool SystemEnabled => true;
public int MaxFps
{
get => _maxFps;
set => _maxFps = Math.Max(1, Math.Min(120, value));
}
public Action? Fire { get; set; }
public void Start()
{
if (_isRunning)
return;
_isRunning = true;
var interval = TimeSpan.FromMilliseconds(1000.0 / _maxFps);
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, interval);
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Fire?.Invoke();
} }
} }

View File

@@ -2,11 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Hosting; using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Hosting;
@@ -46,10 +44,6 @@ public static class LinuxProgramHost
?? new LinuxApplicationOptions(); ?? new LinuxApplicationOptions();
ParseCommandLineOptions(args, options); ParseCommandLineOptions(args, options);
// Initialize GTK for WebView support
GtkHostService.Instance.Initialize(options.Title ?? "MAUI Application", options.Width, options.Height);
DiagnosticLog.Debug("LinuxProgramHost", "GTK initialized for WebView support");
// Create Linux application // Create Linux application
using var linuxApp = new LinuxApplication(); using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options); linuxApp.Initialize(options);
@@ -79,7 +73,7 @@ public static class LinuxProgramHost
// Fallback to demo if no application view is available // Fallback to demo if no application view is available
if (rootView == null) if (rootView == null)
{ {
DiagnosticLog.Warn("LinuxProgramHost", "No application page found. Showing demo UI."); Console.WriteLine("No application page found. Showing demo UI.");
rootView = CreateDemoView(); rootView = CreateDemoView();
} }
@@ -140,8 +134,8 @@ public static class LinuxProgramHost
} }
catch (Exception ex) catch (Exception ex)
{ {
DiagnosticLog.Error("LinuxProgramHost", $"Error rendering application: {ex.Message}"); Console.WriteLine($"Error rendering application: {ex.Message}");
DiagnosticLog.Error("LinuxProgramHost", ex.StackTrace ?? ""); Console.WriteLine(ex.StackTrace);
return null; return null;
} }
} }
@@ -192,33 +186,33 @@ public static class LinuxProgramHost
{ {
Orientation = StackOrientation.Vertical, Orientation = StackOrientation.Vertical,
Spacing = 15, Spacing = 15,
BackgroundColor = Color.FromRgb(0xF5, 0xF5, 0xF5) BackgroundColor = new SKColor(0xF5, 0xF5, 0xF5)
}; };
root.Padding = new Thickness(20, 20, 20, 20); root.Padding = new SKRect(20, 20, 20, 20);
// ========== TITLE ========== // ========== TITLE ==========
root.AddChild(new SkiaLabel root.AddChild(new SkiaLabel
{ {
Text = "OpenMaui Linux Control Demo", Text = "OpenMaui Linux Control Demo",
FontSize = 28, FontSize = 28,
TextColor = Color.FromRgb(0x1A, 0x23, 0x7E), TextColor = new SKColor(0x1A, 0x23, 0x7E),
FontAttributes = FontAttributes.Bold IsBold = true
}); });
root.AddChild(new SkiaLabel root.AddChild(new SkiaLabel
{ {
Text = "All controls rendered using SkiaSharp on X11", Text = "All controls rendered using SkiaSharp on X11",
FontSize = 14, FontSize = 14,
TextColor = Colors.Gray TextColor = SKColors.Gray
}); });
// ========== LABELS SECTION ========== // ========== LABELS SECTION ==========
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Labels")); root.AddChild(CreateSectionHeader("Labels"));
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 }; var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = Colors.Black }); labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black });
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = Colors.Black, FontAttributes = FontAttributes.Bold }); labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true });
labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = Colors.Gray, FontAttributes = FontAttributes.Italic }); labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = SKColors.Gray, IsItalic = true });
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = Color.FromRgb(0xE9, 0x1E, 0x63) }); labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = new SKColor(0xE9, 0x1E, 0x63) });
root.AddChild(labelSection); root.AddChild(labelSection);
// ========== BUTTONS SECTION ========== // ========== BUTTONS SECTION ==========
@@ -227,20 +221,20 @@ public static class LinuxProgramHost
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 }; var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
btnPrimary.BackgroundColor = Color.FromRgb(0x21, 0x96, 0xF3); btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3);
btnPrimary.TextColor = Colors.White; btnPrimary.TextColor = SKColors.White;
var clickCount = 0; var clickCount = 0;
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; }; btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
buttonSection.AddChild(btnPrimary); buttonSection.AddChild(btnPrimary);
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 }; var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
btnSuccess.BackgroundColor = Color.FromRgb(0x4C, 0xAF, 0x50); btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50);
btnSuccess.TextColor = Colors.White; btnSuccess.TextColor = SKColors.White;
buttonSection.AddChild(btnSuccess); buttonSection.AddChild(btnSuccess);
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 }; var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
btnDanger.BackgroundColor = Color.FromRgb(0xF4, 0x43, 0x36); btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36);
btnDanger.TextColor = Colors.White; btnDanger.TextColor = SKColors.White;
buttonSection.AddChild(btnDanger); buttonSection.AddChild(btnDanger);
root.AddChild(buttonSection); root.AddChild(buttonSection);
@@ -255,7 +249,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("SearchBar")); root.AddChild(CreateSectionHeader("SearchBar"));
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." }; var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = Colors.Gray }; var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray };
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}"; searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}"; searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
root.AddChild(searchBar); root.AddChild(searchBar);
@@ -268,7 +262,7 @@ public static class LinuxProgramHost
{ {
Placeholder = "Enter multiple lines of text...", Placeholder = "Enter multiple lines of text...",
FontSize = 14, FontSize = 14,
BackgroundColor = Colors.White BackgroundColor = SKColors.White
}; };
root.AddChild(editor); root.AddChild(editor);
@@ -330,7 +324,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSectionHeader("ProgressBar")); root.AddChild(CreateSectionHeader("ProgressBar"));
var progress = new SkiaProgressBar { Progress = 0.7f }; var progress = new SkiaProgressBar { Progress = 0.7f };
root.AddChild(progress); root.AddChild(progress);
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = Colors.Gray }); root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray });
// ========== ACTIVITYINDICATOR SECTION ========== // ========== ACTIVITYINDICATOR SECTION ==========
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
@@ -338,7 +332,7 @@ public static class LinuxProgramHost
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var activity = new SkiaActivityIndicator { IsRunning = true }; var activity = new SkiaActivityIndicator { IsRunning = true };
activitySection.AddChild(activity); activitySection.AddChild(activity);
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = Colors.Gray }); activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray });
root.AddChild(activitySection); root.AddChild(activitySection);
// ========== PICKER SECTION ========== // ========== PICKER SECTION ==========
@@ -346,7 +340,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSectionHeader("Picker (Dropdown)")); root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
var picker = new SkiaPicker { Title = "Select an item" }; var picker = new SkiaPicker { Title = "Select an item" };
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" }); picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray }; var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}"; picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
root.AddChild(picker); root.AddChild(picker);
root.AddChild(pickerLabel); root.AddChild(pickerLabel);
@@ -355,7 +349,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("DatePicker")); root.AddChild(CreateSectionHeader("DatePicker"));
var datePicker = new SkiaDatePicker { Date = DateTime.Today }; var datePicker = new SkiaDatePicker { Date = DateTime.Today };
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = Colors.Gray }; var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = SKColors.Gray };
datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}"; datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}";
root.AddChild(datePicker); root.AddChild(datePicker);
root.AddChild(dateLabel); root.AddChild(dateLabel);
@@ -364,7 +358,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("TimePicker")); root.AddChild(CreateSectionHeader("TimePicker"));
var timePicker = new SkiaTimePicker(); var timePicker = new SkiaTimePicker();
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = Colors.Gray }; var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = SKColors.Gray };
timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}"; timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}";
root.AddChild(timePicker); root.AddChild(timePicker);
root.AddChild(timeLabel); root.AddChild(timeLabel);
@@ -376,18 +370,18 @@ public static class LinuxProgramHost
{ {
CornerRadius = 8, CornerRadius = 8,
StrokeThickness = 2, StrokeThickness = 2,
Stroke = Color.FromRgb(0x21, 0x96, 0xF3), Stroke = new SKColor(0x21, 0x96, 0xF3),
BackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD) BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD)
}; };
border.SetPadding(15); border.SetPadding(15);
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = Color.FromRgb(0x1A, 0x23, 0x7E) }); border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = new SKColor(0x1A, 0x23, 0x7E) });
root.AddChild(border); root.AddChild(border);
// ========== FRAME SECTION ========== // ========== FRAME SECTION ==========
root.AddChild(CreateSeparator()); root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Frame (with shadow)")); root.AddChild(CreateSectionHeader("Frame (with shadow)"));
var frame = new SkiaFrame(); var frame = new SkiaFrame();
frame.BackgroundColor = Colors.White; frame.BackgroundColor = SKColors.White;
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 }); frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
root.AddChild(frame); root.AddChild(frame);
@@ -401,7 +395,7 @@ public static class LinuxProgramHost
Footer = "End of list" Footer = "End of list"
}; };
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" }); collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray }; var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
collectionView.SelectionChanged += (s, e) => collectionView.SelectionChanged += (s, e) =>
{ {
var selected = e.CurrentSelection.FirstOrDefault(); var selected = e.CurrentSelection.FirstOrDefault();
@@ -419,15 +413,18 @@ public static class LinuxProgramHost
var imgBtn = new SkiaImageButton var imgBtn = new SkiaImageButton
{ {
CornerRadius = 8, CornerRadius = 8,
StrokeColor = Color.FromRgb(0x21, 0x96, 0xF3), StrokeColor = new SKColor(0x21, 0x96, 0xF3),
StrokeThickness = 1, StrokeThickness = 1,
ImageBackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD), BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD),
Padding = new Thickness(10) PaddingLeft = 10,
PaddingRight = 10,
PaddingTop = 10,
PaddingBottom = 10
}; };
// Generate a simple star icon bitmap // Generate a simple star icon bitmap
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3)); var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
imgBtn.Bitmap = iconBitmap; imgBtn.Bitmap = iconBitmap;
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = Colors.Gray }; var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray };
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!"; imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
imageButtonSection.AddChild(imgBtn); imageButtonSection.AddChild(imgBtn);
imageButtonSection.AddChild(imgBtnLabel); imageButtonSection.AddChild(imgBtnLabel);
@@ -443,7 +440,7 @@ public static class LinuxProgramHost
var sampleBitmap = CreateSampleImage(80, 60); var sampleBitmap = CreateSampleImage(80, 60);
img.Bitmap = sampleBitmap; img.Bitmap = sampleBitmap;
imageSection.AddChild(img); imageSection.AddChild(img);
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = Colors.Gray }); imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray });
root.AddChild(imageSection); root.AddChild(imageSection);
// ========== FOOTER ========== // ========== FOOTER ==========
@@ -452,14 +449,14 @@ public static class LinuxProgramHost
{ {
Text = "All 25+ controls are interactive - try them all!", Text = "All 25+ controls are interactive - try them all!",
FontSize = 16, FontSize = 16,
TextColor = Color.FromRgb(0x4C, 0xAF, 0x50), TextColor = new SKColor(0x4C, 0xAF, 0x50),
FontAttributes = FontAttributes.Bold IsBold = true
}); });
root.AddChild(new SkiaLabel root.AddChild(new SkiaLabel
{ {
Text = "Scroll down to see more controls", Text = "Scroll down to see more controls",
FontSize = 12, FontSize = 12,
TextColor = Colors.Gray TextColor = SKColors.Gray
}); });
scroll.Content = root; scroll.Content = root;
@@ -472,14 +469,14 @@ public static class LinuxProgramHost
{ {
Text = text, Text = text,
FontSize = 18, FontSize = 18,
TextColor = Color.FromRgb(0x37, 0x47, 0x4F), TextColor = new SKColor(0x37, 0x47, 0x4F),
FontAttributes = FontAttributes.Bold IsBold = true
}; };
} }
private static SkiaView CreateSeparator() private static SkiaView CreateSeparator()
{ {
var sep = new SkiaLabel { Text = "", BackgroundColor = Color.FromRgb(0xE0, 0xE0, 0xE0), RequestedHeight = 1 }; var sep = new SkiaLabel { Text = "", BackgroundColor = new SKColor(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
return sep; return sep;
} }

View File

@@ -1,47 +0,0 @@
// 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.Animations;
namespace Microsoft.Maui.Platform.Linux.Hosting;
internal class LinuxTicker : ITicker
{
private Timer? _timer;
private bool _isRunning;
private int _maxFps = 60;
public bool IsRunning => _isRunning;
public bool SystemEnabled => true;
public int MaxFps
{
get => _maxFps;
set => _maxFps = Math.Max(1, Math.Min(120, value));
}
public Action? Fire { get; set; }
public void Start()
{
if (!_isRunning)
{
_isRunning = true;
var period = TimeSpan.FromMilliseconds(1000.0 / _maxFps);
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, period);
}
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Fire?.Invoke();
}
}

View File

@@ -1,11 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Hosting;
@@ -39,13 +36,13 @@ public class LinuxViewRenderer
{ {
if (CurrentSkiaShell == null) if (CurrentSkiaShell == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell is null"); Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null");
return false; return false;
} }
// Clean up the route - remove leading // or / // Clean up the route - remove leading // or /
var cleanRoute = route.TrimStart('/'); var cleanRoute = route.TrimStart('/');
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Navigating to: {cleanRoute}"); Console.WriteLine($"[NavigateToRoute] Navigating to: {cleanRoute}");
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{ {
@@ -53,13 +50,13 @@ public class LinuxViewRenderer
if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) || if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) ||
section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase)) section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase))
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Found section {i}: {section.Title}"); Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}");
CurrentSkiaShell.NavigateToSection(i); CurrentSkiaShell.NavigateToSection(i);
return true; return true;
} }
} }
DiagnosticLog.Warn("LinuxViewRenderer", $"NavigateToRoute: Route not found: {cleanRoute}"); Console.WriteLine($"[NavigateToRoute] Route not found: {cleanRoute}");
return false; return false;
} }
@@ -75,40 +72,50 @@ public class LinuxViewRenderer
/// <returns>True if successful</returns> /// <returns>True if successful</returns>
public static bool PushPage(Page page) public static bool PushPage(Page page)
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"PushPage: Pushing page: {page.GetType().Name}"); Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}");
if (CurrentSkiaShell == null) if (CurrentSkiaShell == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentSkiaShell is null"); Console.WriteLine($"[PushPage] CurrentSkiaShell is null");
return false; return false;
} }
if (CurrentRenderer == null) if (CurrentRenderer == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentRenderer is null"); Console.WriteLine($"[PushPage] CurrentRenderer is null");
return false; return false;
} }
try try
{ {
// Render the page through the proper handler system // Render the page content
// This ensures all properties (including BackgroundColor via AppThemeBinding) are mapped SkiaView? pageContent = null;
var skiaPage = CurrentRenderer.RenderPage(page); if (page is ContentPage contentPage && contentPage.Content != null)
if (skiaPage == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: Failed to render page through handler"); pageContent = CurrentRenderer.RenderView(contentPage.Content);
}
if (pageContent == null)
{
Console.WriteLine($"[PushPage] Failed to render page content");
return false; return false;
} }
// Wrap in ScrollView if needed
if (pageContent is not SkiaScrollView)
{
var scrollView = new SkiaScrollView { Content = pageContent };
pageContent = scrollView;
}
// Push onto SkiaShell's navigation stack // Push onto SkiaShell's navigation stack
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail"); CurrentSkiaShell.PushAsync(pageContent, page.Title ?? "Detail");
DiagnosticLog.Debug("LinuxViewRenderer", "PushPage: Successfully pushed page via handler system"); Console.WriteLine($"[PushPage] Successfully pushed page");
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
DiagnosticLog.Error("LinuxViewRenderer", "PushPage failed", ex); Console.WriteLine($"[PushPage] Error: {ex.Message}");
return false; return false;
} }
} }
@@ -119,11 +126,11 @@ public class LinuxViewRenderer
/// <returns>True if successful</returns> /// <returns>True if successful</returns>
public static bool PopPage() public static bool PopPage()
{ {
DiagnosticLog.Debug("LinuxViewRenderer", "PopPage: Popping page"); Console.WriteLine($"[PopPage] Popping page");
if (CurrentSkiaShell == null) if (CurrentSkiaShell == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "PopPage: CurrentSkiaShell is null"); Console.WriteLine($"[PopPage] CurrentSkiaShell is null");
return false; return false;
} }
@@ -155,11 +162,18 @@ public class LinuxViewRenderer
page.Handler?.DisconnectHandler(); page.Handler?.DisconnectHandler();
var handler = page.ToHandler(_mauiContext); var handler = page.ToHandler(_mauiContext);
// The handler's property mappers (e.g., ContentPageHandler.MapContent)
// already set up the content and child handlers - no need to re-render here.
// Re-rendering would disconnect the existing handler hierarchy.
if (handler.PlatformView is SkiaView skiaPage) if (handler.PlatformView is SkiaView skiaPage)
{ {
// For ContentPage, render the content
if (page is ContentPage contentPage && contentPage.Content != null)
{
var contentView = RenderView(contentPage.Content);
if (skiaPage is SkiaPage sp && contentView != null)
{
sp.Content = contentView;
}
}
return skiaPage; return skiaPage;
} }
@@ -184,41 +198,9 @@ public class LinuxViewRenderer
FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked, FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled, FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
_ => ShellFlyoutBehavior.Flyout _ => ShellFlyoutBehavior.Flyout
}, }
MauiShell = shell
}; };
// Apply shell colors based on theme
ApplyShellColors(skiaShell, shell);
// Render flyout header if present
if (shell.FlyoutHeader is View headerView)
{
var skiaHeader = RenderView(headerView);
if (skiaHeader != null)
{
skiaShell.FlyoutHeaderView = skiaHeader;
skiaShell.FlyoutHeaderHeight = (float)(headerView.HeightRequest > 0 ? headerView.HeightRequest : 140.0);
}
}
// Render flyout footer if present, otherwise use version text
if (shell.FlyoutFooter is View footerView)
{
var skiaFooter = RenderView(footerView);
if (skiaFooter != null)
{
skiaShell.FlyoutFooterView = skiaFooter;
skiaShell.FlyoutFooterHeight = (float)(footerView.HeightRequest > 0 ? footerView.HeightRequest : 40.0);
}
}
else
{
// Fallback: use assembly version as footer text
var version = Assembly.GetEntryAssembly()?.GetName().Version;
skiaShell.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}";
}
// Process shell items into sections // Process shell items into sections
foreach (var item in shell.Items) foreach (var item in shell.Items)
{ {
@@ -228,94 +210,45 @@ public class LinuxViewRenderer
// Store reference to SkiaShell for navigation // Store reference to SkiaShell for navigation
CurrentSkiaShell = skiaShell; CurrentSkiaShell = skiaShell;
// Set up content renderer and color refresher delegates
skiaShell.ContentRenderer = CreateShellContentPage;
skiaShell.ColorRefresher = ApplyShellColors;
// Subscribe to MAUI Shell navigation events to update SkiaShell // Subscribe to MAUI Shell navigation events to update SkiaShell
shell.Navigated += OnShellNavigated; shell.Navigated += OnShellNavigated;
shell.Navigating += (s, e) => DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Navigating: {e.Target}"); shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}");
DiagnosticLog.Debug("LinuxViewRenderer", $"Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}"); Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
for (int i = 0; i < skiaShell.Sections.Count; i++) for (int i = 0; i < skiaShell.Sections.Count; i++)
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'"); Console.WriteLine($"[Navigation] Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'");
} }
return skiaShell; return skiaShell;
} }
/// <summary>
/// Applies shell colors based on the current theme (dark/light mode).
/// </summary>
private static void ApplyShellColors(SkiaShell skiaShell, Shell shell)
{
bool isDark = Application.Current?.UserAppTheme == AppTheme.Dark;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Theme is: {(isDark ? "Dark" : "Light")}");
// Flyout background color
if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent)
{
skiaShell.FlyoutBackgroundColor = shell.FlyoutBackgroundColor;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
}
else
{
skiaShell.FlyoutBackgroundColor = isDark
? Color.FromRgb(30, 30, 30)
: Color.FromRgb(255, 255, 255);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
}
// Flyout text color
skiaShell.FlyoutTextColor = isDark
? Color.FromRgb(224, 224, 224)
: Color.FromRgb(33, 33, 33);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutTextColor: {skiaShell.FlyoutTextColor}");
// Content background color
skiaShell.ContentBackgroundColor = isDark
? Color.FromRgb(18, 18, 18)
: Color.FromRgb(250, 250, 250);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
// NavBar background color
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
{
skiaShell.NavBarBackgroundColor = shell.BackgroundColor;
}
else
{
skiaShell.NavBarBackgroundColor = Color.FromRgb(33, 150, 243); // Material blue
}
}
/// <summary> /// <summary>
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly. /// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
/// </summary> /// </summary>
private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}"); Console.WriteLine($"[Navigation] OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}");
if (CurrentSkiaShell == null || CurrentMauiShell == null) if (CurrentSkiaShell == null || CurrentMauiShell == null)
{ {
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell or CurrentMauiShell is null"); Console.WriteLine($"[Navigation] CurrentSkiaShell or CurrentMauiShell is null");
return; return;
} }
// Get the current route from the Shell // Get the current route from the Shell
var currentState = CurrentMauiShell.CurrentState; var currentState = CurrentMauiShell.CurrentState;
var location = currentState?.Location?.OriginalString ?? ""; var location = currentState?.Location?.OriginalString ?? "";
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}"); Console.WriteLine($"[Navigation] Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
// Find the matching section in SkiaShell by route // Find the matching section in SkiaShell by route
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{ {
var section = CurrentSkiaShell.Sections[i]; var section = CurrentSkiaShell.Sections[i];
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Checking section {i}: Route='{section.Route}', Title='{section.Title}'"); Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase))
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by route! Navigating to section {i}"); Console.WriteLine($"[Navigation] Match found by route! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex) if (i != CurrentSkiaShell.CurrentSectionIndex)
{ {
CurrentSkiaShell.NavigateToSection(i); CurrentSkiaShell.NavigateToSection(i);
@@ -324,7 +257,7 @@ public class LinuxViewRenderer
} }
if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase))
{ {
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by title! Navigating to section {i}"); Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex) if (i != CurrentSkiaShell.CurrentSectionIndex)
{ {
CurrentSkiaShell.NavigateToSection(i); CurrentSkiaShell.NavigateToSection(i);
@@ -332,7 +265,7 @@ public class LinuxViewRenderer
return; return;
} }
} }
DiagnosticLog.Warn("LinuxViewRenderer", $"Navigation: No matching section found for location: {location}"); Console.WriteLine($"[Navigation] No matching section found for location: {location}");
} }
/// <summary> /// <summary>
@@ -357,8 +290,7 @@ public class LinuxViewRenderer
var shellContent = new ShellContent var shellContent = new ShellContent
{ {
Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "", Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "",
Route = content.Route ?? "", Route = content.Route ?? ""
MauiShellContent = content
}; };
// Create the page content // Create the page content
@@ -396,8 +328,7 @@ public class LinuxViewRenderer
var shellContent = new ShellContent var shellContent = new ShellContent
{ {
Title = content.Title ?? tab.Title ?? "", Title = content.Title ?? tab.Title ?? "",
Route = content.Route ?? "", Route = content.Route ?? ""
MauiShellContent = content
}; };
var pageContent = CreateShellContentPage(content); var pageContent = CreateShellContentPage(content);
@@ -428,8 +359,7 @@ public class LinuxViewRenderer
var shellContent = new ShellContent var shellContent = new ShellContent
{ {
Title = content.Title ?? "", Title = content.Title ?? "",
Route = content.Route ?? "", Route = content.Route ?? ""
MauiShellContent = content
}; };
var pageContent = CreateShellContentPage(content); var pageContent = CreateShellContentPage(content);
@@ -472,33 +402,17 @@ public class LinuxViewRenderer
var contentView = RenderView(cp.Content); var contentView = RenderView(cp.Content);
if (contentView != null) if (contentView != null)
{ {
// Get page background color if set if (contentView is SkiaScrollView)
Color? bgColor = null;
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
{ {
bgColor = cp.BackgroundColor; return contentView;
DiagnosticLog.Debug("LinuxViewRenderer", $"CreateShellContentPage: Page BackgroundColor: {bgColor}");
}
if (contentView is SkiaScrollView scrollView)
{
if (bgColor != null)
{
scrollView.BackgroundColor = bgColor;
}
return scrollView;
} }
else else
{ {
var newScrollView = new SkiaScrollView var scrollView = new SkiaScrollView
{ {
Content = contentView Content = contentView
}; };
if (bgColor != null) return scrollView;
{
newScrollView.BackgroundColor = bgColor;
}
return newScrollView;
} }
} }
} }
@@ -556,9 +470,28 @@ public class LinuxViewRenderer
return new SkiaLabel return new SkiaLabel
{ {
Text = $"[{view.GetType().Name}]", Text = $"[{view.GetType().Name}]",
TextColor = Colors.Gray, TextColor = SKColors.Gray,
FontSize = 12 FontSize = 12
}; };
} }
} }
/// <summary>
/// Extension methods for MAUI handler creation.
/// </summary>
public static class MauiHandlerExtensions
{
/// <summary>
/// Creates a handler for the view and returns it.
/// </summary>
public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext)
{
var handler = mauiContext.Handlers.GetHandler(element.GetType());
if (handler != null)
{
handler.SetMauiContext(mauiContext);
handler.SetVirtualView(element);
}
return handler!;
}
}

View File

@@ -0,0 +1,190 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Copyright (c) 2025 MarketAlly LLC
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers;
namespace OpenMaui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for configuring OpenMaui Linux platform in a MAUI application.
/// This enables full XAML support by registering Linux-specific handlers.
/// </summary>
public static class MauiAppBuilderExtensions
{
/// <summary>
/// Configures the application to use OpenMaui Linux platform with full XAML support.
/// </summary>
/// <param name="builder">The MAUI app builder.</param>
/// <returns>The configured MAUI app builder.</returns>
/// <example>
/// <code>
/// var builder = MauiApp.CreateBuilder();
/// builder
/// .UseMauiApp&lt;App&gt;()
/// .UseOpenMauiLinux(); // Enable Linux support with XAML
/// </code>
/// </example>
public static MauiAppBuilder UseOpenMauiLinux(this MauiAppBuilder builder)
{
builder.ConfigureMauiHandlers(handlers =>
{
// Register all Linux platform handlers
// These map MAUI virtual views to our Skia platform views
// Basic Controls
handlers.AddHandler<Button, ButtonHandler>();
handlers.AddHandler<Label, LabelHandler>();
handlers.AddHandler<Entry, EntryHandler>();
handlers.AddHandler<Editor, EditorHandler>();
handlers.AddHandler<CheckBox, CheckBoxHandler>();
handlers.AddHandler<Switch, SwitchHandler>();
handlers.AddHandler<RadioButton, RadioButtonHandler>();
// Selection Controls
handlers.AddHandler<Slider, SliderHandler>();
handlers.AddHandler<Stepper, StepperHandler>();
handlers.AddHandler<Picker, PickerHandler>();
handlers.AddHandler<DatePicker, DatePickerHandler>();
handlers.AddHandler<TimePicker, TimePickerHandler>();
// Display Controls
handlers.AddHandler<Image, ImageHandler>();
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<ActivityIndicator, ActivityIndicatorHandler>();
handlers.AddHandler<ProgressBar, ProgressBarHandler>();
// Layout Controls
handlers.AddHandler<Border, BorderHandler>();
// Collection Controls
handlers.AddHandler<CollectionView, CollectionViewHandler>();
// Navigation Controls
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
handlers.AddHandler<TabbedPage, TabbedPageHandler>();
handlers.AddHandler<FlyoutPage, FlyoutPageHandler>();
handlers.AddHandler<Shell, ShellHandler>();
// Page Controls
handlers.AddHandler<Page, PageHandler>();
handlers.AddHandler<ContentPage, PageHandler>();
// Graphics
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
// Search
handlers.AddHandler<SearchBar, SearchBarHandler>();
// Web
handlers.AddHandler<WebView, WebViewHandler>();
// Window
handlers.AddHandler<Window, WindowHandler>();
});
// Register Linux-specific services
builder.Services.AddSingleton<ILinuxPlatformServices, LinuxPlatformServices>();
return builder;
}
/// <summary>
/// Configures the application to use OpenMaui Linux with custom handler configuration.
/// </summary>
/// <param name="builder">The MAUI app builder.</param>
/// <param name="configureHandlers">Action to configure additional handlers.</param>
/// <returns>The configured MAUI app builder.</returns>
public static MauiAppBuilder UseOpenMauiLinux(
this MauiAppBuilder builder,
Action<IMauiHandlersCollection>? configureHandlers)
{
builder.UseOpenMauiLinux();
if (configureHandlers != null)
{
builder.ConfigureMauiHandlers(configureHandlers);
}
return builder;
}
}
/// <summary>
/// Interface for Linux platform services.
/// </summary>
public interface ILinuxPlatformServices
{
/// <summary>
/// Gets the display server type (X11 or Wayland).
/// </summary>
DisplayServerType DisplayServer { get; }
/// <summary>
/// Gets the current DPI scale factor.
/// </summary>
float ScaleFactor { get; }
/// <summary>
/// Gets whether high contrast mode is enabled.
/// </summary>
bool IsHighContrastEnabled { get; }
}
/// <summary>
/// Display server types supported by OpenMaui.
/// </summary>
public enum DisplayServerType
{
/// <summary>X11 display server.</summary>
X11,
/// <summary>Wayland display server.</summary>
Wayland,
/// <summary>Auto-detected display server.</summary>
Auto
}
/// <summary>
/// Implementation of Linux platform services.
/// </summary>
internal class LinuxPlatformServices : ILinuxPlatformServices
{
public DisplayServerType DisplayServer => DetectDisplayServer();
public float ScaleFactor => DetectScaleFactor();
public bool IsHighContrastEnabled => DetectHighContrast();
private static DisplayServerType DetectDisplayServer()
{
var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY");
if (!string.IsNullOrEmpty(waylandDisplay))
return DisplayServerType.Wayland;
var display = Environment.GetEnvironmentVariable("DISPLAY");
if (!string.IsNullOrEmpty(display))
return DisplayServerType.X11;
return DisplayServerType.Auto;
}
private static float DetectScaleFactor()
{
// Try GDK_SCALE first
var gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE");
if (float.TryParse(gdkScale, out var scale))
return scale;
// Default to 1.0
return 1.0f;
}
private static bool DetectHighContrast()
{
var highContrast = Environment.GetEnvironmentVariable("GTK_THEME");
return highContrast?.Contains("HighContrast", StringComparison.OrdinalIgnoreCase) ?? false;
}
}

View File

@@ -1,124 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for creating MAUI handlers on Linux.
/// Maps MAUI types to Linux-specific handlers with fallback to MAUI defaults.
/// </summary>
public static class MauiHandlerExtensions
{
private static readonly Dictionary<Type, Func<IElementHandler>> LinuxHandlerMap = new Dictionary<Type, Func<IElementHandler>>
{
[typeof(Button)] = () => new TextButtonHandler(),
[typeof(Label)] = () => new LabelHandler(),
[typeof(Entry)] = () => new EntryHandler(),
[typeof(Editor)] = () => new EditorHandler(),
[typeof(CheckBox)] = () => new CheckBoxHandler(),
[typeof(Switch)] = () => new SwitchHandler(),
[typeof(Slider)] = () => new SliderHandler(),
[typeof(Stepper)] = () => new StepperHandler(),
[typeof(ProgressBar)] = () => new ProgressBarHandler(),
[typeof(ActivityIndicator)] = () => new ActivityIndicatorHandler(),
[typeof(Picker)] = () => new PickerHandler(),
[typeof(DatePicker)] = () => new DatePickerHandler(),
[typeof(TimePicker)] = () => new TimePickerHandler(),
[typeof(SearchBar)] = () => new SearchBarHandler(),
[typeof(RadioButton)] = () => new RadioButtonHandler(),
[typeof(WebView)] = () => new GtkWebViewHandler(),
[typeof(Image)] = () => new ImageHandler(),
[typeof(ImageButton)] = () => new ImageButtonHandler(),
[typeof(BoxView)] = () => new BoxViewHandler(),
[typeof(Frame)] = () => new FrameHandler(),
[typeof(Border)] = () => new BorderHandler(),
[typeof(ContentView)] = () => new BorderHandler(),
[typeof(ScrollView)] = () => new ScrollViewHandler(),
[typeof(Grid)] = () => new GridHandler(),
[typeof(StackLayout)] = () => new StackLayoutHandler(),
[typeof(VerticalStackLayout)] = () => new StackLayoutHandler(),
[typeof(HorizontalStackLayout)] = () => new StackLayoutHandler(),
[typeof(AbsoluteLayout)] = () => new LayoutHandler(),
[typeof(FlexLayout)] = () => new LayoutHandler(),
[typeof(CollectionView)] = () => new CollectionViewHandler(),
[typeof(ListView)] = () => new CollectionViewHandler(),
[typeof(Page)] = () => new PageHandler(),
[typeof(ContentPage)] = () => new ContentPageHandler(),
[typeof(NavigationPage)] = () => new NavigationPageHandler(),
[typeof(Shell)] = () => new ShellHandler(),
[typeof(FlyoutPage)] = () => new FlyoutPageHandler(),
[typeof(TabbedPage)] = () => new TabbedPageHandler(),
[typeof(Application)] = () => new ApplicationHandler(),
[typeof(Microsoft.Maui.Controls.Window)] = () => new WindowHandler(),
[typeof(GraphicsView)] = () => new GraphicsViewHandler()
};
/// <summary>
/// Creates an element handler for the given element.
/// </summary>
public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext)
{
return CreateHandler(element, mauiContext)!;
}
/// <summary>
/// Creates a view handler for the given view.
/// </summary>
public static IViewHandler? ToViewHandler(this IView view, IMauiContext mauiContext)
{
var handler = CreateHandler((IElement)view, mauiContext);
return handler as IViewHandler;
}
private static IElementHandler? CreateHandler(IElement element, IMauiContext mauiContext)
{
Type type = element.GetType();
IElementHandler? handler = null;
// First, try exact type match
if (LinuxHandlerMap.TryGetValue(type, out Func<IElementHandler>? factory))
{
handler = factory();
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using Linux handler for {type.Name}: {handler.GetType().Name}");
}
else
{
// Try to find a base type match
Type? bestMatch = null;
Func<IElementHandler>? bestFactory = null;
foreach (var kvp in LinuxHandlerMap)
{
if (kvp.Key.IsAssignableFrom(type) && (bestMatch == null || bestMatch.IsAssignableFrom(kvp.Key)))
{
bestMatch = kvp.Key;
bestFactory = kvp.Value;
}
}
if (bestFactory != null)
{
handler = bestFactory();
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using Linux handler (via base {bestMatch!.Name}) for {type.Name}: {handler.GetType().Name}");
}
}
// Fall back to MAUI's default handler
if (handler == null)
{
handler = mauiContext.Handlers.GetHandler(type);
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using MAUI handler for {type.Name}: {handler?.GetType().Name ?? "null"}");
}
if (handler != null)
{
handler.SetMauiContext(mauiContext);
handler.SetVirtualView(element);
}
return handler;
}
}

View File

@@ -1,18 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Hosting;
public class ScopedLinuxMauiContext : IMauiContext
{
private readonly LinuxMauiContext _parent;
public IServiceProvider Services => _parent.Services;
public IMauiHandlersFactory Handlers => _parent.Handlers;
public ScopedLinuxMauiContext(LinuxMauiContext parent)
{
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
}
}

View File

@@ -1,25 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
[StructLayout(LayoutKind.Explicit)]
public struct ClientMessageData
{
[FieldOffset(0)]
public long L0;
[FieldOffset(8)]
public long L1;
[FieldOffset(16)]
public long L2;
[FieldOffset(24)]
public long L3;
[FieldOffset(32)]
public long L4;
}

View File

@@ -1,345 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
/// <summary>
/// P/Invoke bindings for WebKitGTK library.
/// WebKitGTK provides a full-featured web browser engine for Linux.
/// </summary>
public static class WebKitGtk
{
private const string WebKit2Lib = "libwebkit2gtk-4.1.so.0";
private const string GtkLib = "libgtk-3.so.0";
private const string GObjectLib = "libgobject-2.0.so.0";
private const string GLibLib = "libglib-2.0.so.0";
#region GTK Initialization
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool gtk_init_check(ref int argc, ref IntPtr argv);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_main();
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_main_quit();
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool gtk_events_pending();
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_main_iteration();
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool gtk_main_iteration_do(bool blocking);
#endregion
#region GTK Window
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gtk_window_new(int type);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_window_set_default_size(IntPtr window, int width, int height);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_window_set_decorated(IntPtr window, bool decorated);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_window_move(IntPtr window, int x, int y);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_window_resize(IntPtr window, int width, int height);
#endregion
#region GTK Widget
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_show_all(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_show(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_hide(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_destroy(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_set_size_request(IntPtr widget, int width, int height);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_realize(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gtk_widget_get_window(IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_widget_set_can_focus(IntPtr widget, bool canFocus);
#endregion
#region GTK Container
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_container_add(IntPtr container, IntPtr widget);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void gtk_container_remove(IntPtr container, IntPtr widget);
#endregion
#region GTK Plug (for embedding in X11 windows)
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gtk_plug_new(ulong socketId);
[DllImport(GtkLib, CallingConvention = CallingConvention.Cdecl)]
public static extern ulong gtk_plug_get_id(IntPtr plug);
#endregion
#region WebKitWebView
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_new();
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_new_with_context(IntPtr context);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_load_uri(IntPtr webView, [MarshalAs(UnmanagedType.LPUTF8Str)] string uri);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_load_html(IntPtr webView,
[MarshalAs(UnmanagedType.LPUTF8Str)] string content,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? baseUri);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_reload(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_stop_loading(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_go_back(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_go_forward(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool webkit_web_view_can_go_back(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool webkit_web_view_can_go_forward(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_get_uri(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_get_title(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern double webkit_web_view_get_estimated_load_progress(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern bool webkit_web_view_is_loading(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_web_view_run_javascript(IntPtr webView,
[MarshalAs(UnmanagedType.LPUTF8Str)] string script,
IntPtr cancellable,
IntPtr callback,
IntPtr userData);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_run_javascript_finish(IntPtr webView,
IntPtr result,
out IntPtr error);
#endregion
#region WebKitSettings
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_view_get_settings(IntPtr webView);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_enable_javascript(IntPtr settings, bool enabled);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_user_agent(IntPtr settings,
[MarshalAs(UnmanagedType.LPUTF8Str)] string userAgent);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_settings_get_user_agent(IntPtr settings);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_enable_developer_extras(IntPtr settings, bool enabled);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_javascript_can_access_clipboard(IntPtr settings, bool enabled);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_enable_webgl(IntPtr settings, bool enabled);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_allow_file_access_from_file_urls(IntPtr settings, bool enabled);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_settings_set_allow_universal_access_from_file_urls(IntPtr settings, bool enabled);
#endregion
#region WebKitWebContext
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_context_get_default();
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_context_new();
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_web_context_get_cookie_manager(IntPtr context);
#endregion
#region WebKitCookieManager
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_cookie_manager_set_accept_policy(IntPtr cookieManager, int policy);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_cookie_manager_set_persistent_storage(IntPtr cookieManager,
[MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
int storage);
// Cookie accept policies
public const int WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS = 0;
public const int WEBKIT_COOKIE_POLICY_ACCEPT_NEVER = 1;
public const int WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY = 2;
// Cookie persistent storage types
public const int WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT = 0;
public const int WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE = 1;
#endregion
#region WebKitNavigationAction
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_navigation_action_get_request(IntPtr action);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int webkit_navigation_action_get_navigation_type(IntPtr action);
#endregion
#region WebKitURIRequest
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr webkit_uri_request_get_uri(IntPtr request);
#endregion
#region WebKitPolicyDecision
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_policy_decision_use(IntPtr decision);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_policy_decision_ignore(IntPtr decision);
[DllImport(WebKit2Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern void webkit_policy_decision_download(IntPtr decision);
#endregion
#region GObject Signal Connection
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void GCallback();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate bool DecidePolicyCallback(IntPtr webView, IntPtr decision, int decisionType, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LoadFailedCallback(IntPtr webView, int loadEvent, IntPtr failingUri, IntPtr error, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void NotifyCallback(IntPtr webView, IntPtr paramSpec, IntPtr userData);
[DllImport(GObjectLib, CallingConvention = CallingConvention.Cdecl)]
public static extern ulong g_signal_connect_data(IntPtr instance,
[MarshalAs(UnmanagedType.LPUTF8Str)] string detailedSignal,
Delegate handler,
IntPtr data,
IntPtr destroyData,
int connectFlags);
[DllImport(GObjectLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void g_signal_handler_disconnect(IntPtr instance, ulong handlerId);
[DllImport(GObjectLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void g_object_unref(IntPtr obj);
#endregion
#region GLib Memory
[DllImport(GLibLib, CallingConvention = CallingConvention.Cdecl)]
public static extern void g_free(IntPtr mem);
#endregion
#region WebKit Load Events
public const int WEBKIT_LOAD_STARTED = 0;
public const int WEBKIT_LOAD_REDIRECTED = 1;
public const int WEBKIT_LOAD_COMMITTED = 2;
public const int WEBKIT_LOAD_FINISHED = 3;
#endregion
#region WebKit Policy Decision Types
public const int WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION = 0;
public const int WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION = 1;
public const int WEBKIT_POLICY_DECISION_TYPE_RESPONSE = 2;
#endregion
#region Helper Methods
/// <summary>
/// Converts a native UTF-8 string pointer to a managed string.
/// </summary>
public static string? PtrToStringUtf8(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
return null;
return Marshal.PtrToStringUTF8(ptr);
}
/// <summary>
/// Processes pending GTK events without blocking.
/// </summary>
public static void ProcessGtkEvents()
{
while (gtk_events_pending())
{
gtk_main_iteration_do(false);
}
}
#endregion
}

View File

@@ -1,239 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
internal static partial class X11
{
private const string LibX11 = "libX11.so.6";
public const int ZPixmap = 2;
// Event types
public const int ClientMessage = 33;
// Event masks for XSendEvent
public const long SubstructureRedirectMask = 1L << 20;
public const long SubstructureNotifyMask = 1L << 19;
[LibraryImport(LibX11)]
public static partial IntPtr XOpenDisplay(IntPtr displayName);
[LibraryImport(LibX11)]
public static partial int XCloseDisplay(IntPtr display);
[LibraryImport(LibX11)]
public static partial int XDefaultScreen(IntPtr display);
[LibraryImport(LibX11)]
public static partial IntPtr XRootWindow(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDisplayWidth(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDisplayHeight(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDefaultDepth(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultVisual(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultColormap(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XFlush(IntPtr display);
[LibraryImport(LibX11)]
public static partial int XSync(IntPtr display, [MarshalAs(UnmanagedType.Bool)] bool discard);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateSimpleWindow(
IntPtr display, IntPtr parent,
int x, int y, uint width, uint height,
uint borderWidth, ulong border, ulong background);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateWindow(
IntPtr display, IntPtr parent,
int x, int y, uint width, uint height, uint borderWidth,
int depth, uint windowClass, IntPtr visual,
ulong valueMask, ref XSetWindowAttributes attributes);
[LibraryImport(LibX11)]
public static partial int XDestroyWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XMapWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XUnmapWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XMoveWindow(IntPtr display, IntPtr window, int x, int y);
[LibraryImport(LibX11)]
public static partial int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
[LibraryImport(LibX11)]
public static partial int XIconifyWindow(IntPtr display, IntPtr window, int screen);
[LibraryImport(LibX11)]
public static partial int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height);
[LibraryImport(LibX11, StringMarshalling = StringMarshalling.Utf8)]
public static partial int XStoreName(IntPtr display, IntPtr window, string windowName);
[LibraryImport(LibX11)]
public static partial int XSetClassHint(IntPtr display, IntPtr window, ref XClassHint classHint);
[LibraryImport(LibX11)]
public static partial int XRaiseWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XLowerWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XSelectInput(IntPtr display, IntPtr window, long eventMask);
[LibraryImport(LibX11)]
public static partial int XNextEvent(IntPtr display, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XPeekEvent(IntPtr display, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XPending(IntPtr display);
[LibraryImport(LibX11)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool XCheckTypedWindowEvent(IntPtr display, IntPtr window, int eventType, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XSendEvent(
IntPtr display, IntPtr window,
[MarshalAs(UnmanagedType.Bool)] bool propagate,
long eventMask, ref XEvent eventSend);
[LibraryImport(LibX11)]
public static partial ulong XKeycodeToKeysym(IntPtr display, int keycode, int index);
[LibraryImport(LibX11)]
public static partial int XLookupString(
ref XKeyEvent keyEvent, IntPtr bufferReturn, int bytesBuffer,
out ulong keysymReturn, IntPtr statusInOut);
[LibraryImport(LibX11)]
public static partial int XGrabKeyboard(
IntPtr display, IntPtr grabWindow,
[MarshalAs(UnmanagedType.Bool)] bool ownerEvents,
int pointerMode, int keyboardMode, ulong time);
[LibraryImport(LibX11)]
public static partial int XUngrabKeyboard(IntPtr display, ulong time);
[LibraryImport(LibX11)]
public static partial int XGrabPointer(
IntPtr display, IntPtr grabWindow,
[MarshalAs(UnmanagedType.Bool)] bool ownerEvents,
uint eventMask, int pointerMode, int keyboardMode,
IntPtr confineTo, IntPtr cursor, ulong time);
[LibraryImport(LibX11)]
public static partial int XUngrabPointer(IntPtr display, ulong time);
[LibraryImport(LibX11)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool XQueryPointer(
IntPtr display, IntPtr window,
out IntPtr rootReturn, out IntPtr childReturn,
out int rootX, out int rootY,
out int winX, out int winY,
out uint maskReturn);
[LibraryImport(LibX11)]
public static partial int XWarpPointer(
IntPtr display, IntPtr srcWindow, IntPtr destWindow,
int srcX, int srcY, uint srcWidth, uint srcHeight,
int destX, int destY);
[LibraryImport(LibX11, StringMarshalling = StringMarshalling.Utf8)]
public static partial IntPtr XInternAtom(IntPtr display, string atomName, [MarshalAs(UnmanagedType.Bool)] bool onlyIfExists);
[LibraryImport(LibX11)]
public static partial int XChangeProperty(
IntPtr display, IntPtr window, IntPtr property, IntPtr type,
int format, int mode, IntPtr data, int nelements);
[LibraryImport(LibX11)]
public static partial int XGetWindowProperty(
IntPtr display, IntPtr window, IntPtr property,
long longOffset, long longLength,
[MarshalAs(UnmanagedType.Bool)] bool delete, IntPtr reqType,
out IntPtr actualTypeReturn, out int actualFormatReturn,
out IntPtr nitemsReturn, out IntPtr bytesAfterReturn,
out IntPtr propReturn);
[LibraryImport(LibX11)]
public static partial int XDeleteProperty(IntPtr display, IntPtr window, IntPtr property);
[LibraryImport(LibX11)]
public static partial int XSetSelectionOwner(IntPtr display, IntPtr selection, IntPtr owner, ulong time);
[LibraryImport(LibX11)]
public static partial IntPtr XGetSelectionOwner(IntPtr display, IntPtr selection);
[LibraryImport(LibX11)]
public static partial int XConvertSelection(
IntPtr display, IntPtr selection, IntPtr target,
IntPtr property, IntPtr requestor, ulong time);
[LibraryImport(LibX11)]
public static partial int XFree(IntPtr data);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateGC(IntPtr display, IntPtr drawable, ulong valueMask, IntPtr values);
[LibraryImport(LibX11)]
public static partial int XFreeGC(IntPtr display, IntPtr gc);
[LibraryImport(LibX11)]
public static partial int XCopyArea(
IntPtr display, IntPtr src, IntPtr dest, IntPtr gc,
int srcX, int srcY, uint width, uint height, int destX, int destY);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateFontCursor(IntPtr display, uint shape);
[LibraryImport(LibX11)]
public static partial int XFreeCursor(IntPtr display, IntPtr cursor);
[LibraryImport(LibX11)]
public static partial int XDefineCursor(IntPtr display, IntPtr window, IntPtr cursor);
[LibraryImport(LibX11)]
public static partial int XUndefineCursor(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XConnectionNumber(IntPtr display);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateImage(
IntPtr display, IntPtr visual, uint depth, int format, int offset,
IntPtr data, uint width, uint height, int bitmapPad, int bytesPerLine);
[LibraryImport(LibX11)]
public static partial int XPutImage(
IntPtr display, IntPtr drawable, IntPtr gc, IntPtr image,
int srcX, int srcY, int destX, int destY, uint width, uint height);
[LibraryImport(LibX11)]
public static partial int XDestroyImage(IntPtr image);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultGC(IntPtr display, int screen);
}

482
Interop/X11Interop.cs Normal file
View File

@@ -0,0 +1,482 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
/// <summary>
/// P/Invoke declarations for X11 library functions.
/// </summary>
internal static partial class X11
{
private const string LibX11 = "libX11.so.6";
private const string LibXext = "libXext.so.6";
#region Display and Screen
[LibraryImport(LibX11)]
public static partial IntPtr XOpenDisplay(IntPtr displayName);
[LibraryImport(LibX11)]
public static partial int XCloseDisplay(IntPtr display);
[LibraryImport(LibX11)]
public static partial int XDefaultScreen(IntPtr display);
[LibraryImport(LibX11)]
public static partial IntPtr XRootWindow(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDisplayWidth(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDisplayHeight(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XDefaultDepth(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultVisual(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultColormap(IntPtr display, int screenNumber);
[LibraryImport(LibX11)]
public static partial int XFlush(IntPtr display);
[LibraryImport(LibX11)]
public static partial int XSync(IntPtr display, [MarshalAs(UnmanagedType.Bool)] bool discard);
#endregion
#region Window Creation and Management
[LibraryImport(LibX11)]
public static partial IntPtr XCreateSimpleWindow(
IntPtr display,
IntPtr parent,
int x, int y,
uint width, uint height,
uint borderWidth,
ulong border,
ulong background);
[LibraryImport(LibX11)]
public static partial IntPtr XCreateWindow(
IntPtr display,
IntPtr parent,
int x, int y,
uint width, uint height,
uint borderWidth,
int depth,
uint windowClass,
IntPtr visual,
ulong valueMask,
ref XSetWindowAttributes attributes);
[LibraryImport(LibX11)]
public static partial int XDestroyWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XMapWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XUnmapWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XMoveWindow(IntPtr display, IntPtr window, int x, int y);
[LibraryImport(LibX11)]
public static partial int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
[LibraryImport(LibX11)]
public static partial int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height);
[LibraryImport(LibX11, StringMarshalling = StringMarshalling.Utf8)]
public static partial int XStoreName(IntPtr display, IntPtr window, string windowName);
[LibraryImport(LibX11)]
public static partial int XRaiseWindow(IntPtr display, IntPtr window);
[LibraryImport(LibX11)]
public static partial int XLowerWindow(IntPtr display, IntPtr window);
#endregion
#region Event Handling
[LibraryImport(LibX11)]
public static partial int XSelectInput(IntPtr display, IntPtr window, long eventMask);
[LibraryImport(LibX11)]
public static partial int XNextEvent(IntPtr display, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XPeekEvent(IntPtr display, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XPending(IntPtr display);
[LibraryImport(LibX11)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool XCheckTypedWindowEvent(IntPtr display, IntPtr window, int eventType, out XEvent eventReturn);
[LibraryImport(LibX11)]
public static partial int XSendEvent(IntPtr display, IntPtr window, [MarshalAs(UnmanagedType.Bool)] bool propagate, long eventMask, ref XEvent eventSend);
#endregion
#region Keyboard
[LibraryImport(LibX11)]
public static partial ulong XKeycodeToKeysym(IntPtr display, int keycode, int index);
[LibraryImport(LibX11)]
public static partial int XLookupString(ref XKeyEvent keyEvent, IntPtr bufferReturn, int bytesBuffer, out ulong keysymReturn, IntPtr statusInOut);
[LibraryImport(LibX11)]
public static partial int XGrabKeyboard(IntPtr display, IntPtr grabWindow, [MarshalAs(UnmanagedType.Bool)] bool ownerEvents, int pointerMode, int keyboardMode, ulong time);
[LibraryImport(LibX11)]
public static partial int XUngrabKeyboard(IntPtr display, ulong time);
#endregion
#region Mouse/Pointer
[LibraryImport(LibX11)]
public static partial int XGrabPointer(IntPtr display, IntPtr grabWindow, [MarshalAs(UnmanagedType.Bool)] bool ownerEvents, uint eventMask, int pointerMode, int keyboardMode, IntPtr confineTo, IntPtr cursor, ulong time);
[LibraryImport(LibX11)]
public static partial int XUngrabPointer(IntPtr display, ulong time);
[LibraryImport(LibX11)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool XQueryPointer(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr childReturn, out int rootX, out int rootY, out int winX, out int winY, out uint maskReturn);
[LibraryImport(LibX11)]
public static partial int XWarpPointer(IntPtr display, IntPtr srcWindow, IntPtr destWindow, int srcX, int srcY, uint srcWidth, uint srcHeight, int destX, int destY);
#endregion
#region Atoms and Properties
[LibraryImport(LibX11, StringMarshalling = StringMarshalling.Utf8)]
public static partial IntPtr XInternAtom(IntPtr display, string atomName, [MarshalAs(UnmanagedType.Bool)] bool onlyIfExists);
[LibraryImport(LibX11)]
public static partial int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr data, int nelements);
[LibraryImport(LibX11)]
public static partial int XGetWindowProperty(IntPtr display, IntPtr window, IntPtr property, long longOffset, long longLength, [MarshalAs(UnmanagedType.Bool)] bool delete, IntPtr reqType, out IntPtr actualTypeReturn, out int actualFormatReturn, out IntPtr nitemsReturn, out IntPtr bytesAfterReturn, out IntPtr propReturn);
[LibraryImport(LibX11)]
public static partial int XDeleteProperty(IntPtr display, IntPtr window, IntPtr property);
#endregion
#region Clipboard/Selection
[LibraryImport(LibX11)]
public static partial int XSetSelectionOwner(IntPtr display, IntPtr selection, IntPtr owner, ulong time);
[LibraryImport(LibX11)]
public static partial IntPtr XGetSelectionOwner(IntPtr display, IntPtr selection);
[LibraryImport(LibX11)]
public static partial int XConvertSelection(IntPtr display, IntPtr selection, IntPtr target, IntPtr property, IntPtr requestor, ulong time);
#endregion
#region Memory
[LibraryImport(LibX11)]
public static partial int XFree(IntPtr data);
#endregion
#region Graphics Context
[LibraryImport(LibX11)]
public static partial IntPtr XCreateGC(IntPtr display, IntPtr drawable, ulong valueMask, IntPtr values);
[LibraryImport(LibX11)]
public static partial int XFreeGC(IntPtr display, IntPtr gc);
[LibraryImport(LibX11)]
public static partial int XCopyArea(IntPtr display, IntPtr src, IntPtr dest, IntPtr gc, int srcX, int srcY, uint width, uint height, int destX, int destY);
#endregion
#region Cursor
[LibraryImport(LibX11)]
public static partial IntPtr XCreateFontCursor(IntPtr display, uint shape);
[LibraryImport(LibX11)]
public static partial int XFreeCursor(IntPtr display, IntPtr cursor);
[LibraryImport(LibX11)]
public static partial int XDefineCursor(IntPtr display, IntPtr window, IntPtr cursor);
[LibraryImport(LibX11)]
public static partial int XUndefineCursor(IntPtr display, IntPtr window);
#endregion
#region Connection
[LibraryImport(LibX11)]
public static partial int XConnectionNumber(IntPtr display);
#endregion
#region Image Functions
[LibraryImport(LibX11)]
public static partial IntPtr XCreateImage(IntPtr display, IntPtr visual, uint depth, int format,
int offset, IntPtr data, uint width, uint height, int bitmapPad, int bytesPerLine);
[LibraryImport(LibX11)]
public static partial int XPutImage(IntPtr display, IntPtr drawable, IntPtr gc, IntPtr image,
int srcX, int srcY, int destX, int destY, uint width, uint height);
[LibraryImport(LibX11)]
public static partial int XDestroyImage(IntPtr image);
[LibraryImport(LibX11)]
public static partial IntPtr XDefaultGC(IntPtr display, int screen);
public const int ZPixmap = 2;
#endregion
}
#region X11 Structures
[StructLayout(LayoutKind.Sequential)]
public struct XSetWindowAttributes
{
public IntPtr BackgroundPixmap;
public ulong BackgroundPixel;
public IntPtr BorderPixmap;
public ulong BorderPixel;
public int BitGravity;
public int WinGravity;
public int BackingStore;
public ulong BackingPlanes;
public ulong BackingPixel;
public int SaveUnder;
public long EventMask;
public long DoNotPropagateMask;
public int OverrideRedirect;
public IntPtr Colormap;
public IntPtr Cursor;
}
[StructLayout(LayoutKind.Explicit, Size = 192)]
public struct XEvent
{
[FieldOffset(0)] public int Type;
[FieldOffset(0)] public XKeyEvent KeyEvent;
[FieldOffset(0)] public XButtonEvent ButtonEvent;
[FieldOffset(0)] public XMotionEvent MotionEvent;
[FieldOffset(0)] public XConfigureEvent ConfigureEvent;
[FieldOffset(0)] public XExposeEvent ExposeEvent;
[FieldOffset(0)] public XClientMessageEvent ClientMessageEvent;
[FieldOffset(0)] public XCrossingEvent CrossingEvent;
[FieldOffset(0)] public XFocusChangeEvent FocusChangeEvent;
}
[StructLayout(LayoutKind.Sequential)]
public struct XKeyEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X, Y;
public int XRoot, YRoot;
public uint State;
public uint Keycode;
public int SameScreen;
}
[StructLayout(LayoutKind.Sequential)]
public struct XButtonEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X, Y;
public int XRoot, YRoot;
public uint State;
public uint Button;
public int SameScreen;
}
[StructLayout(LayoutKind.Sequential)]
public struct XMotionEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X, Y;
public int XRoot, YRoot;
public uint State;
public byte IsHint;
public int SameScreen;
}
[StructLayout(LayoutKind.Sequential)]
public struct XConfigureEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Event;
public IntPtr Window;
public int X, Y;
public int Width, Height;
public int BorderWidth;
public IntPtr Above;
public int OverrideRedirect;
}
[StructLayout(LayoutKind.Sequential)]
public struct XExposeEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public int X, Y;
public int Width, Height;
public int Count;
}
[StructLayout(LayoutKind.Sequential)]
public struct XClientMessageEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr MessageType;
public int Format;
public ClientMessageData Data;
}
[StructLayout(LayoutKind.Explicit)]
public struct ClientMessageData
{
[FieldOffset(0)] public long L0;
[FieldOffset(8)] public long L1;
[FieldOffset(16)] public long L2;
[FieldOffset(24)] public long L3;
[FieldOffset(32)] public long L4;
}
[StructLayout(LayoutKind.Sequential)]
public struct XCrossingEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X, Y;
public int XRoot, YRoot;
public int Mode;
public int Detail;
public int SameScreen;
public int Focus;
public uint State;
}
[StructLayout(LayoutKind.Sequential)]
public struct XFocusChangeEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public int Mode;
public int Detail;
}
#endregion
#region X11 Constants
public static class XEventType
{
public const int KeyPress = 2;
public const int KeyRelease = 3;
public const int ButtonPress = 4;
public const int ButtonRelease = 5;
public const int MotionNotify = 6;
public const int EnterNotify = 7;
public const int LeaveNotify = 8;
public const int FocusIn = 9;
public const int FocusOut = 10;
public const int Expose = 12;
public const int ConfigureNotify = 22;
public const int ClientMessage = 33;
}
public static class XEventMask
{
public const long KeyPressMask = 1L << 0;
public const long KeyReleaseMask = 1L << 1;
public const long ButtonPressMask = 1L << 2;
public const long ButtonReleaseMask = 1L << 3;
public const long EnterWindowMask = 1L << 4;
public const long LeaveWindowMask = 1L << 5;
public const long PointerMotionMask = 1L << 6;
public const long ExposureMask = 1L << 15;
public const long StructureNotifyMask = 1L << 17;
public const long FocusChangeMask = 1L << 21;
}
public static class XWindowClass
{
public const uint InputOutput = 1;
public const uint InputOnly = 2;
}
public static class XCursorShape
{
public const uint XC_left_ptr = 68;
public const uint XC_hand2 = 60;
public const uint XC_xterm = 152;
public const uint XC_watch = 150;
public const uint XC_crosshair = 34;
}
#endregion

View File

@@ -1,23 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Interop;
public struct XButtonEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X;
public int Y;
public int XRoot;
public int YRoot;
public uint State;
public uint Button;
public int SameScreen;
}

View File

@@ -1,13 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
[StructLayout(LayoutKind.Sequential)]
public struct XClassHint
{
public IntPtr res_name;
public IntPtr res_class;
}

View File

@@ -1,16 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Interop;
public struct XClientMessageEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr MessageType;
public int Format;
public ClientMessageData Data;
}

View File

@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Interop;
public struct XConfigureEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Event;
public IntPtr Window;
public int X;
public int Y;
public int Width;
public int Height;
public int BorderWidth;
public IntPtr Above;
public int OverrideRedirect;
}

View File

@@ -1,25 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Interop;
public struct XCrossingEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X;
public int Y;
public int XRoot;
public int YRoot;
public int Mode;
public int Detail;
public int SameScreen;
public int Focus;
public uint State;
}

Some files were not shown because too many files have changed in this diff Show More