Compare commits
126 Commits
v1.0.0-preview.2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c6a3f4acc4 | |||
| 8a5ad459ed | |||
| 840ad0ce8f | |||
| 967713c01a | |||
| 6b45f28b4e | |||
| 3412cb982e | |||
| c5221ba580 | |||
| 077abc2feb | |||
| ee60b983a4 | |||
| 0fb0051a24 | |||
| 15ced2ac55 | |||
| e55230c441 | |||
| 08e0c4d2b9 | |||
| d9d3218f17 | |||
| 996ffb67c0 | |||
| 4d880e77d5 | |||
| ade3a8a47d | |||
| b7424194f4 | |||
| 0f093bdb1e | |||
| 675673f026 | |||
| f1e3630d1b | |||
| 38c48fc99f | |||
| f48f909ee2 | |||
| 433787ff80 | |||
| ed89b6494d | |||
| 7830356f24 | |||
| 5415c71e9e | |||
| 2c5036596e | |||
| 0c2508d715 | |||
| f4422d4af1 | |||
| 88679dfae8 | |||
| 10a66cd399 | |||
| 9451611c3a | |||
| dc52f7f2bc | |||
| f1a368a6c2 | |||
| ad12779b73 | |||
| 7d2ac327a3 | |||
| f62d4aa5f2 | |||
| a367365ce5 | |||
| aad915ad86 | |||
| 5443c7c22a | |||
| 5a915ca06a | |||
| 47a5fc8c01 | |||
| 523de9d8b9 | |||
| b07228922f | |||
| 4c70118be6 | |||
| 868ce1fcae | |||
| b1749b1347 | |||
| d8bdf5472f | |||
| 7a1241cbf2 | |||
| 27e2dc7136 | |||
| 8b1c733943 | |||
| 331d6839d9 | |||
| bf2f380f56 | |||
| a11b081510 | |||
| c27f073938 | |||
| 59bcb9244e | |||
| c21ff803fe | |||
| 7056ab7e4e | |||
| 1b1619de06 | |||
| 538d4bad65 | |||
| 85b3c22dc7 | |||
| 675466a0f5 | |||
| 870382097b | |||
| 20fcbd78e3 | |||
| 0094cdef45 | |||
| 8f1eba7fbe | |||
| 436c9d60cb | |||
| 083f110cf4 | |||
| f263ee96b3 | |||
| 271f8d7fa9 | |||
| a8c8939a3f | |||
| 71a37da1a4 | |||
| 81f7d9c90e | |||
| d5a7560479 | |||
| 209c56e592 | |||
| 9a49185183 | |||
| a2800464c8 | |||
| aab53ee919 | |||
| 4a64927c12 | |||
| bc80436a34 | |||
| 01270c6938 | |||
| 18ab0abe97 | |||
| 8a36a44341 | |||
| 95e7d0c90b | |||
| c60453ea31 | |||
| 7b22d67920 | |||
| f6eadaad57 | |||
| 6007b84e7a | |||
| 4d225a43ef | |||
| 55d4a6eaad | |||
| 5613df6031 | |||
| d34f4e1fea | |||
| a471cb071a | |||
| 317aaaf23c | |||
| 6f0d10935c | |||
| fd9043f749 | |||
| b0b3746968 | |||
| 1cdf66c44b | |||
| d3feaa8964 | |||
| f7043ab9c7 | |||
| e02af03be0 | |||
| d0d8e92dad | |||
| 1e84c6168a | |||
|
|
10a061777e | ||
| 0dcb76695e | |||
| 10222090fd | |||
| b18d5a11f3 | |||
|
|
2719ddf720 | ||
|
|
7e58513ab3 | ||
|
|
a450daa86f | ||
|
|
c8840f2e8b | ||
|
|
f0dbd29b58 | ||
|
|
a4f04f4966 | ||
|
|
a03c600864 | ||
|
|
0c460c1395 | ||
|
|
0dd7a2d3fb | ||
| afbf8f6782 | |||
| 02b3da17d4 | |||
|
|
299914d077 | ||
|
|
ed09456d57 | ||
|
|
1d55ac672a | ||
|
|
f945d2a537 | ||
|
|
1d9338d823 | ||
|
|
ae5c9ab738 | ||
|
|
d238dde5a4 |
39
.gitea/workflows/ci.yml
Normal file
39
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# OpenMaui Linux CI Pipeline
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, final, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
name: Build (Linux)
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test --configuration Release --no-build --verbosity normal
|
||||
continue-on-error: true
|
||||
|
||||
- name: Pack NuGet
|
||||
run: dotnet pack --configuration Release --no-build -o ./nupkg
|
||||
|
||||
- name: Upload NuGet packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: ./nupkg/*.nupkg
|
||||
67
.gitea/workflows/release.yml
Normal file
67
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# OpenMaui Linux Release - Publish to Package Registries
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Publish
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test --configuration Release --no-build --verbosity normal
|
||||
continue-on-error: true
|
||||
|
||||
- name: Pack NuGet package
|
||||
run: dotnet pack --configuration Release --no-build -o ./nupkg -p:PackageVersion=${{ steps.version.outputs.VERSION }}
|
||||
|
||||
- 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
|
||||
if: ${{ secrets.NUGET_API_KEY != '' }}
|
||||
run: |
|
||||
for pkg in ./nupkg/*.nupkg; do
|
||||
dotnet nuget push "$pkg" \
|
||||
--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
5
.gitignore
vendored
@@ -47,3 +47,8 @@ coverage*.xml
|
||||
|
||||
# Publish output
|
||||
publish/
|
||||
mauiplan.md
|
||||
|
||||
# Development artifacts
|
||||
out.xml
|
||||
MERGE_TRACKING.md
|
||||
|
||||
8
.notes/note-1770523035702-znjlto6vn.json
Normal file
8
.notes/note-1770523035702-znjlto6vn.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
55
.notes/series-1769749919894-6e18cc.json
Normal file
55
.notes/series-1769749919894-6e18cc.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750134166-c6e172.json
Normal file
55
.notes/series-1769750134166-c6e172.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750314106-d25473.json
Normal file
55
.notes/series-1769750314106-d25473.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750550451-66bac3.json
Normal file
55
.notes/series-1769750550451-66bac3.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750791920-5db729.json
Normal file
55
.notes/series-1769750791920-5db729.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769751056430-116f9a.json
Normal file
55
.notes/series-1769751056430-116f9a.json
Normal file
File diff suppressed because one or more lines are too long
59
.notes/series-1769751324711-9007af.json
Normal file
59
.notes/series-1769751324711-9007af.json
Normal file
File diff suppressed because one or more lines are too long
182
AnimationManager.cs
Normal file
182
AnimationManager.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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 LLC](https://marketally.com) 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 Pte Ltd](https://marketally.sg) under the leadership of David H. Friedel Jr.
|
||||
|
||||
This document provides guidelines and information for contributors.
|
||||
|
||||
|
||||
39
Controls/EntryExtensions.cs
Normal file
39
Controls/EntryExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
20
Converters/ColorExtensions.cs
Normal file
20
Converters/ColorExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
237
Converters/SKColorTypeConverter.cs
Normal file
237
Converters/SKColorTypeConverter.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
// 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 SkiaSharp;
|
||||
using Microsoft.Maui.Graphics;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Type converter for converting between MAUI Color and SKColor.
|
||||
/// Enables XAML styling with Color values that get applied to Skia controls.
|
||||
/// </summary>
|
||||
public class SKColorTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) ||
|
||||
sourceType == typeof(Color) ||
|
||||
base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) ||
|
||||
destinationType == typeof(Color) ||
|
||||
base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is Color mauiColor)
|
||||
{
|
||||
return ToSKColor(mauiColor);
|
||||
}
|
||||
|
||||
if (value is string str)
|
||||
{
|
||||
return ParseColor(str);
|
||||
}
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (value is SKColor skColor)
|
||||
{
|
||||
if (destinationType == typeof(string))
|
||||
{
|
||||
return $"#{skColor.Alpha:X2}{skColor.Red:X2}{skColor.Green:X2}{skColor.Blue:X2}";
|
||||
}
|
||||
|
||||
if (destinationType == typeof(Color))
|
||||
{
|
||||
return ToMauiColor(skColor);
|
||||
}
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a MAUI Color to an SKColor.
|
||||
/// </summary>
|
||||
public static SKColor ToSKColor(Color mauiColor)
|
||||
{
|
||||
return new SKColor(
|
||||
(byte)(mauiColor.Red * 255),
|
||||
(byte)(mauiColor.Green * 255),
|
||||
(byte)(mauiColor.Blue * 255),
|
||||
(byte)(mauiColor.Alpha * 255));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an SKColor to a MAUI Color.
|
||||
/// </summary>
|
||||
public static Color ToMauiColor(SKColor skColor)
|
||||
{
|
||||
return new Color(
|
||||
skColor.Red / 255f,
|
||||
skColor.Green / 255f,
|
||||
skColor.Blue / 255f,
|
||||
skColor.Alpha / 255f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a color string (hex, named, or rgb format).
|
||||
/// </summary>
|
||||
private static SKColor ParseColor(string colorString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(colorString))
|
||||
return SKColors.Black;
|
||||
|
||||
colorString = colorString.Trim();
|
||||
|
||||
// Try hex format
|
||||
if (colorString.StartsWith("#"))
|
||||
{
|
||||
return SKColor.Parse(colorString);
|
||||
}
|
||||
|
||||
// Try named colors
|
||||
var namedColor = GetNamedColor(colorString.ToLowerInvariant());
|
||||
if (namedColor.HasValue)
|
||||
return namedColor.Value;
|
||||
|
||||
// Try rgb/rgba format
|
||||
if (colorString.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ParseRgbColor(colorString);
|
||||
}
|
||||
|
||||
// Fallback to SKColor.Parse
|
||||
if (SKColor.TryParse(colorString, out var parsed))
|
||||
return parsed;
|
||||
|
||||
return SKColors.Black;
|
||||
}
|
||||
|
||||
private static SKColor? GetNamedColor(string name) => name switch
|
||||
{
|
||||
"transparent" => SKColors.Transparent,
|
||||
"black" => SKColors.Black,
|
||||
"white" => SKColors.White,
|
||||
"red" => SKColors.Red,
|
||||
"green" => SKColors.Green,
|
||||
"blue" => SKColors.Blue,
|
||||
"yellow" => SKColors.Yellow,
|
||||
"cyan" => SKColors.Cyan,
|
||||
"magenta" => SKColors.Magenta,
|
||||
"gray" or "grey" => SKColors.Gray,
|
||||
"darkgray" or "darkgrey" => SKColors.DarkGray,
|
||||
"lightgray" or "lightgrey" => SKColors.LightGray,
|
||||
"orange" => new SKColor(0xFF, 0xA5, 0x00),
|
||||
"pink" => new SKColor(0xFF, 0xC0, 0xCB),
|
||||
"purple" => new SKColor(0x80, 0x00, 0x80),
|
||||
"brown" => new SKColor(0xA5, 0x2A, 0x2A),
|
||||
"navy" => new SKColor(0x00, 0x00, 0x80),
|
||||
"teal" => new SKColor(0x00, 0x80, 0x80),
|
||||
"olive" => new SKColor(0x80, 0x80, 0x00),
|
||||
"silver" => new SKColor(0xC0, 0xC0, 0xC0),
|
||||
"maroon" => new SKColor(0x80, 0x00, 0x00),
|
||||
"lime" => new SKColor(0x00, 0xFF, 0x00),
|
||||
"aqua" => new SKColor(0x00, 0xFF, 0xFF),
|
||||
"fuchsia" => new SKColor(0xFF, 0x00, 0xFF),
|
||||
"gold" => new SKColor(0xFF, 0xD7, 0x00),
|
||||
"coral" => new SKColor(0xFF, 0x7F, 0x50),
|
||||
"salmon" => new SKColor(0xFA, 0x80, 0x72),
|
||||
"crimson" => new SKColor(0xDC, 0x14, 0x3C),
|
||||
"indigo" => new SKColor(0x4B, 0x00, 0x82),
|
||||
"violet" => new SKColor(0xEE, 0x82, 0xEE),
|
||||
"turquoise" => new SKColor(0x40, 0xE0, 0xD0),
|
||||
"tan" => new SKColor(0xD2, 0xB4, 0x8C),
|
||||
"chocolate" => new SKColor(0xD2, 0x69, 0x1E),
|
||||
"tomato" => new SKColor(0xFF, 0x63, 0x47),
|
||||
"steelblue" => new SKColor(0x46, 0x82, 0xB4),
|
||||
"skyblue" => new SKColor(0x87, 0xCE, 0xEB),
|
||||
"slategray" or "slategrey" => new SKColor(0x70, 0x80, 0x90),
|
||||
"seagreen" => new SKColor(0x2E, 0x8B, 0x57),
|
||||
"royalblue" => new SKColor(0x41, 0x69, 0xE1),
|
||||
"plum" => new SKColor(0xDD, 0xA0, 0xDD),
|
||||
"peru" => new SKColor(0xCD, 0x85, 0x3F),
|
||||
"orchid" => new SKColor(0xDA, 0x70, 0xD6),
|
||||
"orangered" => new SKColor(0xFF, 0x45, 0x00),
|
||||
"olivedrab" => new SKColor(0x6B, 0x8E, 0x23),
|
||||
"midnightblue" => new SKColor(0x19, 0x19, 0x70),
|
||||
"mediumblue" => new SKColor(0x00, 0x00, 0xCD),
|
||||
"limegreen" => new SKColor(0x32, 0xCD, 0x32),
|
||||
"hotpink" => new SKColor(0xFF, 0x69, 0xB4),
|
||||
"honeydew" => new SKColor(0xF0, 0xFF, 0xF0),
|
||||
"greenyellow" => new SKColor(0xAD, 0xFF, 0x2F),
|
||||
"forestgreen" => new SKColor(0x22, 0x8B, 0x22),
|
||||
"firebrick" => new SKColor(0xB2, 0x22, 0x22),
|
||||
"dodgerblue" => new SKColor(0x1E, 0x90, 0xFF),
|
||||
"deeppink" => new SKColor(0xFF, 0x14, 0x93),
|
||||
"deepskyblue" => new SKColor(0x00, 0xBF, 0xFF),
|
||||
"darkviolet" => new SKColor(0x94, 0x00, 0xD3),
|
||||
"darkturquoise" => new SKColor(0x00, 0xCE, 0xD1),
|
||||
"darkslategray" or "darkslategrey" => new SKColor(0x2F, 0x4F, 0x4F),
|
||||
"darkred" => new SKColor(0x8B, 0x00, 0x00),
|
||||
"darkorange" => new SKColor(0xFF, 0x8C, 0x00),
|
||||
"darkolivegreen" => new SKColor(0x55, 0x6B, 0x2F),
|
||||
"darkmagenta" => new SKColor(0x8B, 0x00, 0x8B),
|
||||
"darkkhaki" => new SKColor(0xBD, 0xB7, 0x6B),
|
||||
"darkgreen" => new SKColor(0x00, 0x64, 0x00),
|
||||
"darkgoldenrod" => new SKColor(0xB8, 0x86, 0x0B),
|
||||
"darkcyan" => new SKColor(0x00, 0x8B, 0x8B),
|
||||
"darkblue" => new SKColor(0x00, 0x00, 0x8B),
|
||||
"cornflowerblue" => new SKColor(0x64, 0x95, 0xED),
|
||||
"cadetblue" => new SKColor(0x5F, 0x9E, 0xA0),
|
||||
"blueviolet" => new SKColor(0x8A, 0x2B, 0xE2),
|
||||
"azure" => new SKColor(0xF0, 0xFF, 0xFF),
|
||||
"aquamarine" => new SKColor(0x7F, 0xFF, 0xD4),
|
||||
"aliceblue" => new SKColor(0xF0, 0xF8, 0xFF),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static SKColor ParseRgbColor(string colorString)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isRgba = colorString.StartsWith("rgba", StringComparison.OrdinalIgnoreCase);
|
||||
var startIndex = colorString.IndexOf('(');
|
||||
var endIndex = colorString.IndexOf(')');
|
||||
|
||||
if (startIndex == -1 || endIndex == -1)
|
||||
return SKColors.Black;
|
||||
|
||||
var values = colorString.Substring(startIndex + 1, endIndex - startIndex - 1)
|
||||
.Split(',')
|
||||
.Select(v => v.Trim())
|
||||
.ToArray();
|
||||
|
||||
if (values.Length < 3)
|
||||
return SKColors.Black;
|
||||
|
||||
var r = byte.Parse(values[0]);
|
||||
var g = byte.Parse(values[1]);
|
||||
var b = byte.Parse(values[2]);
|
||||
byte a = 255;
|
||||
|
||||
if (isRgba && values.Length >= 4)
|
||||
{
|
||||
var alphaValue = float.Parse(values[3], CultureInfo.InvariantCulture);
|
||||
a = (byte)(alphaValue <= 1 ? alphaValue * 255 : alphaValue);
|
||||
}
|
||||
|
||||
return new SKColor(r, g, b, a);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return SKColors.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Converters/SKPointTypeConverter.cs
Normal file
75
Converters/SKPointTypeConverter.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
139
Converters/SKRectTypeConverter.cs
Normal file
139
Converters/SKRectTypeConverter.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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 SkiaSharp;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Type converter for converting between MAUI Thickness and SKRect (for padding/margin).
|
||||
/// Enables XAML styling with Thickness values that get applied to Skia controls.
|
||||
/// </summary>
|
||||
public class SKRectTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) ||
|
||||
sourceType == typeof(Thickness) ||
|
||||
sourceType == typeof(double) ||
|
||||
sourceType == typeof(float) ||
|
||||
base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) ||
|
||||
destinationType == typeof(Thickness) ||
|
||||
base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is Thickness thickness)
|
||||
{
|
||||
return ThicknessToSKRect(thickness);
|
||||
}
|
||||
|
||||
if (value is double d)
|
||||
{
|
||||
return new SKRect((float)d, (float)d, (float)d, (float)d);
|
||||
}
|
||||
|
||||
if (value is float f)
|
||||
{
|
||||
return new SKRect(f, f, f, f);
|
||||
}
|
||||
|
||||
if (value is string str)
|
||||
{
|
||||
return ParseRect(str);
|
||||
}
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (value is SKRect rect)
|
||||
{
|
||||
if (destinationType == typeof(string))
|
||||
{
|
||||
return $"{rect.Left},{rect.Top},{rect.Right},{rect.Bottom}";
|
||||
}
|
||||
|
||||
if (destinationType == typeof(Thickness))
|
||||
{
|
||||
return SKRectToThickness(rect);
|
||||
}
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a MAUI Thickness to an SKRect (used as padding storage).
|
||||
/// </summary>
|
||||
public static SKRect ThicknessToSKRect(Thickness thickness)
|
||||
{
|
||||
return new SKRect(
|
||||
(float)thickness.Left,
|
||||
(float)thickness.Top,
|
||||
(float)thickness.Right,
|
||||
(float)thickness.Bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an SKRect (used as padding storage) to a MAUI Thickness.
|
||||
/// </summary>
|
||||
public static Thickness SKRectToThickness(SKRect rect)
|
||||
{
|
||||
return new Thickness(rect.Left, rect.Top, rect.Right, rect.Bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into an SKRect for padding/margin.
|
||||
/// Supports formats: "uniform", "horizontal,vertical", "left,top,right,bottom"
|
||||
/// </summary>
|
||||
private static SKRect ParseRect(string str)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
return SKRect.Empty;
|
||||
|
||||
str = str.Trim();
|
||||
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
// Uniform padding
|
||||
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform))
|
||||
{
|
||||
return new SKRect(uniform, uniform, uniform, uniform);
|
||||
}
|
||||
}
|
||||
else if (parts.Length == 2)
|
||||
{
|
||||
// Horizontal, Vertical
|
||||
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var horizontal) &&
|
||||
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var vertical))
|
||||
{
|
||||
return new SKRect(horizontal, vertical, horizontal, vertical);
|
||||
}
|
||||
}
|
||||
else if (parts.Length == 4)
|
||||
{
|
||||
// Left, Top, Right, Bottom
|
||||
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var left) &&
|
||||
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var top) &&
|
||||
float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var right) &&
|
||||
float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bottom))
|
||||
{
|
||||
return new SKRect(left, top, right, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
return SKRect.Empty;
|
||||
}
|
||||
}
|
||||
82
Converters/SKSizeTypeConverter.cs
Normal file
82
Converters/SKSizeTypeConverter.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
40
Converters/SKTypeExtensions.cs
Normal file
40
Converters/SKTypeExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
77
Dispatching/LinuxDispatcher.cs
Normal file
77
Dispatching/LinuxDispatcher.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
15
Dispatching/LinuxDispatcherProvider.cs
Normal file
15
Dispatching/LinuxDispatcherProvider.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
110
Dispatching/LinuxDispatcherTimer.cs
Normal file
110
Dispatching/LinuxDispatcherTimer.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
11
DisplayServerType.cs
Normal file
11
DisplayServerType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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
|
||||
}
|
||||
53
Easing.cs
Normal file
53
Easing.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,43 +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;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
||||
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
|
||||
[nameof(IActivityIndicator.Color)] = MapColor,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -38,6 +39,19 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
||||
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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -49,7 +63,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (activityIndicator.Color is not null)
|
||||
handler.PlatformView.Color = activityIndicator.Color.ToSKColor();
|
||||
handler.PlatformView.Color = activityIndicator.Color;
|
||||
}
|
||||
|
||||
public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
||||
@@ -58,7 +72,14 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
||||
|
||||
if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = activityIndicator.IsEnabled;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
137
Handlers/ApplicationHandler.cs
Normal file
137
Handlers/ApplicationHandler.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for MAUI Application on Linux.
|
||||
/// Bridges the MAUI Application lifecycle with LinuxApplication.
|
||||
/// </summary>
|
||||
public partial class ApplicationHandler : ElementHandler<IApplication, LinuxApplicationContext>
|
||||
{
|
||||
public static IPropertyMapper<IApplication, ApplicationHandler> Mapper =
|
||||
new PropertyMapper<IApplication, ApplicationHandler>(ElementHandler.ElementMapper)
|
||||
{
|
||||
};
|
||||
|
||||
public static CommandMapper<IApplication, ApplicationHandler> CommandMapper =
|
||||
new(ElementHandler.ElementCommandMapper)
|
||||
{
|
||||
[nameof(IApplication.OpenWindow)] = MapOpenWindow,
|
||||
[nameof(IApplication.CloseWindow)] = MapCloseWindow,
|
||||
};
|
||||
|
||||
public ApplicationHandler() : base(Mapper, CommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
|
||||
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override LinuxApplicationContext CreatePlatformElement()
|
||||
{
|
||||
return new LinuxApplicationContext();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(LinuxApplicationContext platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.Application = VirtualView;
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(LinuxApplicationContext platformView)
|
||||
{
|
||||
platformView.Application = null;
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args)
|
||||
{
|
||||
if (args is IWindow window)
|
||||
{
|
||||
handler.PlatformView?.OpenWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args)
|
||||
{
|
||||
if (args is IWindow window)
|
||||
{
|
||||
handler.PlatformView?.CloseWindow(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform context for the MAUI Application on Linux.
|
||||
/// Manages windows and the application lifecycle.
|
||||
/// </summary>
|
||||
public class LinuxApplicationContext
|
||||
{
|
||||
private readonly List<IWindow> _windows = new();
|
||||
private IApplication? _application;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MAUI Application.
|
||||
/// </summary>
|
||||
public IApplication? Application
|
||||
{
|
||||
get => _application;
|
||||
set
|
||||
{
|
||||
_application = value;
|
||||
if (_application != null)
|
||||
{
|
||||
// Initialize windows from the application
|
||||
foreach (var window in _application.Windows)
|
||||
{
|
||||
if (!_windows.Contains(window))
|
||||
{
|
||||
_windows.Add(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of open windows.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IWindow> Windows => _windows;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a window and creates its handler.
|
||||
/// </summary>
|
||||
public void OpenWindow(IWindow window)
|
||||
{
|
||||
if (!_windows.Contains(window))
|
||||
{
|
||||
_windows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a window and cleans up its handler.
|
||||
/// </summary>
|
||||
public void CloseWindow(IWindow window)
|
||||
{
|
||||
_windows.Remove(window);
|
||||
|
||||
if (_windows.Count == 0)
|
||||
{
|
||||
// Last window closed, stop the application
|
||||
LinuxApplication.Current?.MainWindow?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window of the application.
|
||||
/// </summary>
|
||||
public IWindow? MainWindow => _windows.Count > 0 ? _windows[0] : null;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -20,8 +22,17 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
[nameof(IBorderView.Content)] = MapContent,
|
||||
[nameof(IBorderStroke.Stroke)] = MapStroke,
|
||||
[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
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
["WidthRequest"] = MapWidthRequest,
|
||||
["HeightRequest"] = MapHeightRequest,
|
||||
};
|
||||
|
||||
public static CommandMapper<IBorderView, BorderHandler> CommandMapper =
|
||||
@@ -46,22 +57,70 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
protected override void ConnectHandler(SkiaBorder 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)
|
||||
{
|
||||
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 MapContent(BorderHandler handler, IBorderView border)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||
|
||||
handler.PlatformView.ClearChildren();
|
||||
|
||||
if (border.PresentedContent?.Handler?.PlatformView is SkiaView skiaContent)
|
||||
var content = border.PresentedContent;
|
||||
if (content != null)
|
||||
{
|
||||
handler.PlatformView.AddChild(skiaContent);
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
DiagnosticLog.Debug("BorderHandler", $"Creating handler for content: {content.GetType().Name}");
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
DiagnosticLog.Debug("BorderHandler", $"Adding content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.AddChild(skiaContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +130,14 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
|
||||
if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.Stroke = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.Stroke = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapStrokeThickness(BorderHandler handler, IBorderView border)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.StrokeThickness = (float)border.StrokeThickness;
|
||||
handler.PlatformView.StrokeThickness = border.StrokeThickness;
|
||||
}
|
||||
|
||||
public static void MapBackground(BorderHandler handler, IBorderView border)
|
||||
@@ -87,7 +146,23 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
|
||||
if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(BorderHandler handler, IBorderView border)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (border is VisualElement ve)
|
||||
{
|
||||
var bgColor = ve.BackgroundColor;
|
||||
DiagnosticLog.Debug("BorderHandler", $"MapBackgroundColor: {bgColor}");
|
||||
if (bgColor != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = bgColor;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +171,123 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
var padding = border.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;
|
||||
handler.PlatformView.PaddingLeft = padding.Left;
|
||||
handler.PlatformView.PaddingTop = padding.Top;
|
||||
handler.PlatformView.PaddingRight = padding.Right;
|
||||
handler.PlatformView.PaddingBottom = padding.Bottom;
|
||||
}
|
||||
|
||||
public static void MapStrokeShape(BorderHandler handler, IBorderView border)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// StrokeShape is on the Border control class, not IBorderView interface
|
||||
if (border is not Border borderControl) return;
|
||||
|
||||
var shape = borderControl.StrokeShape;
|
||||
|
||||
// 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)
|
||||
{
|
||||
var cornerRadius = roundRect.CornerRadius;
|
||||
handler.PlatformView.CornerRadius = cornerRadius.TopLeft;
|
||||
}
|
||||
else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle)
|
||||
{
|
||||
handler.PlatformView.CornerRadius = 0.0;
|
||||
}
|
||||
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
|
||||
{
|
||||
handler.PlatformView.CornerRadius = double.MaxValue;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
77
Handlers/BoxViewHandler.cs
Normal file
77
Handlers/BoxViewHandler.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for BoxView on Linux.
|
||||
/// </summary>
|
||||
public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
|
||||
{
|
||||
public static IPropertyMapper<BoxView, BoxViewHandler> Mapper =
|
||||
new PropertyMapper<BoxView, BoxViewHandler>(ViewMapper)
|
||||
{
|
||||
[nameof(BoxView.Color)] = MapColor,
|
||||
[nameof(BoxView.CornerRadius)] = MapCornerRadius,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public BoxViewHandler() : base(Mapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SkiaBoxView CreatePlatformView()
|
||||
{
|
||||
return new SkiaBoxView();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (boxView.Color != null)
|
||||
{
|
||||
handler.PlatformView.Color = boxView.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView)
|
||||
{
|
||||
handler.PlatformView.CornerRadius = boxView.CornerRadius;
|
||||
}
|
||||
|
||||
public static void MapBackground(BoxViewHandler handler, BoxView boxView)
|
||||
{
|
||||
if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView)
|
||||
{
|
||||
if (boxView.BackgroundColor != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = boxView.BackgroundColor;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +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 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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = 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)
|
||||
{
|
||||
handler.PlatformView.IsEnabled = button.IsEnabled;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// 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.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -20,6 +22,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -47,6 +50,32 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
platformView.Clicked += OnClicked;
|
||||
platformView.Pressed += OnPressed;
|
||||
platformView.Released += OnReleased;
|
||||
|
||||
// Manually map all properties on connect since MAUI may not trigger updates
|
||||
// for properties that were set before handler connection
|
||||
if (VirtualView != null)
|
||||
{
|
||||
MapStrokeColor(this, VirtualView);
|
||||
MapStrokeThickness(this, VirtualView);
|
||||
MapCornerRadius(this, VirtualView);
|
||||
MapBackground(this, VirtualView);
|
||||
MapPadding(this, VirtualView);
|
||||
MapIsEnabled(this, VirtualView);
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaButton platformView)
|
||||
@@ -67,13 +96,13 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
|
||||
var strokeColor = button.StrokeColor;
|
||||
if (strokeColor is not null)
|
||||
handler.PlatformView.BorderColor = strokeColor.ToSKColor();
|
||||
handler.PlatformView.BorderColor = strokeColor;
|
||||
}
|
||||
|
||||
public static void MapStrokeThickness(ButtonHandler handler, IButton button)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
|
||||
handler.PlatformView.BorderWidth = button.StrokeThickness;
|
||||
}
|
||||
|
||||
public static void MapCornerRadius(ButtonHandler handler, IButton button)
|
||||
@@ -88,7 +117,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
|
||||
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
// Set BackgroundColor (MAUI Color type)
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,11 +127,18 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
var padding = button.Padding;
|
||||
handler.PlatformView.Padding = new SKRect(
|
||||
(float)padding.Left,
|
||||
(float)padding.Top,
|
||||
(float)padding.Right,
|
||||
(float)padding.Bottom);
|
||||
handler.PlatformView.Padding = new Thickness(
|
||||
padding.Left,
|
||||
padding.Top,
|
||||
padding.Right,
|
||||
padding.Bottom);
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(ButtonHandler handler, IButton button)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = button.IsEnabled;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +161,33 @@ public partial class TextButtonHandler : ButtonHandler
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaButton platformView)
|
||||
{
|
||||
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler START");
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Manually map text properties on connect since MAUI may not trigger updates
|
||||
// for properties that were set before handler connection
|
||||
if (VirtualView is ITextButton textButton)
|
||||
{
|
||||
MapText(this, textButton);
|
||||
MapTextColor(this, textButton);
|
||||
MapFont(this, textButton);
|
||||
MapCharacterSpacing(this, textButton);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -135,7 +199,7 @@ public partial class TextButtonHandler : ButtonHandler
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (button.TextColor is not null)
|
||||
handler.PlatformView.TextColor = button.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = button.TextColor;
|
||||
}
|
||||
|
||||
public static void MapFont(TextButtonHandler handler, ITextButton button)
|
||||
@@ -144,18 +208,23 @@ public partial class TextButtonHandler : ButtonHandler
|
||||
|
||||
var font = button.Font;
|
||||
if (font.Size > 0)
|
||||
handler.PlatformView.FontSize = (float)font.Size;
|
||||
handler.PlatformView.FontSize = font.Size;
|
||||
|
||||
if (!string.IsNullOrEmpty(font.Family))
|
||||
handler.PlatformView.FontFamily = font.Family;
|
||||
|
||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
||||
// 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(TextButtonHandler handler, ITextButton button)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.CharacterSpacing = (float)button.CharacterSpacing;
|
||||
handler.PlatformView.CharacterSpacing = button.CharacterSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
255
Handlers/CarouselViewHandler.cs
Normal file
255
Handlers/CarouselViewHandler.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +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 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,
|
||||
};
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
|
||||
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
|
||||
[nameof(ICheckBox.Foreground)] = MapForeground,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||
};
|
||||
|
||||
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -70,7 +73,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
|
||||
|
||||
if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.CheckColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.CheckColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +83,41 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
|
||||
|
||||
if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.Color = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.VerticalOptions = checkBox.VerticalLayoutAlignment switch
|
||||
{
|
||||
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
|
||||
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
|
||||
Primitives.LayoutAlignment.End => LayoutOptions.End,
|
||||
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
|
||||
_ => LayoutOptions.Fill
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.HorizontalOptions = checkBox.HorizontalLayoutAlignment switch
|
||||
{
|
||||
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
|
||||
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
|
||||
Primitives.LayoutAlignment.End => LayoutOptions.End,
|
||||
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
|
||||
_ => LayoutOptions.Start
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -15,6 +17,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
/// </summary>
|
||||
public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCollectionView>
|
||||
{
|
||||
private bool _isUpdatingSelection;
|
||||
|
||||
public static IPropertyMapper<CollectionView, CollectionViewHandler> Mapper =
|
||||
new PropertyMapper<CollectionView, CollectionViewHandler>(ViewHandler.ViewMapper)
|
||||
{
|
||||
@@ -36,6 +40,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout,
|
||||
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(CollectionView.BackgroundColor)] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public static CommandMapper<CollectionView, CollectionViewHandler> CommandMapper =
|
||||
@@ -76,21 +81,34 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
|
||||
private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null) return;
|
||||
if (VirtualView is null || _isUpdatingSelection) return;
|
||||
|
||||
// Update virtual view selection
|
||||
if (VirtualView.SelectionMode == SelectionMode.Single)
|
||||
try
|
||||
{
|
||||
VirtualView.SelectedItem = e.CurrentSelection.FirstOrDefault();
|
||||
}
|
||||
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
|
||||
{
|
||||
// Clear and update selected items
|
||||
VirtualView.SelectedItems.Clear();
|
||||
foreach (var item in e.CurrentSelection)
|
||||
_isUpdatingSelection = true;
|
||||
|
||||
// Update virtual view selection
|
||||
if (VirtualView.SelectionMode == SelectionMode.Single)
|
||||
{
|
||||
VirtualView.SelectedItems.Add(item);
|
||||
var newItem = e.CurrentSelection.FirstOrDefault();
|
||||
if (!Equals(VirtualView.SelectedItem, newItem))
|
||||
{
|
||||
VirtualView.SelectedItem = newItem;
|
||||
}
|
||||
}
|
||||
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
|
||||
{
|
||||
// Clear and update selected items
|
||||
VirtualView.SelectedItems.Clear();
|
||||
foreach (var item in e.CurrentSelection)
|
||||
{
|
||||
VirtualView.SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +125,49 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
|
||||
private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e)
|
||||
{
|
||||
// Item tap is handled through selection
|
||||
if (VirtualView is null || _isUpdatingSelection) return;
|
||||
|
||||
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)
|
||||
@@ -118,7 +178,68 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
|
||||
public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView)
|
||||
{
|
||||
handler.PlatformView?.Invalidate();
|
||||
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||
|
||||
var template = collectionView.ItemTemplate;
|
||||
if (template != null)
|
||||
{
|
||||
// Set up a renderer that creates views from the DataTemplate
|
||||
handler.PlatformView.ItemViewCreator = (item) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create view from template
|
||||
var content = template.CreateContent();
|
||||
if (content is View view)
|
||||
{
|
||||
// Set binding context FIRST so bindings evaluate
|
||||
view.BindingContext = item;
|
||||
|
||||
// Force binding evaluation by accessing the visual tree
|
||||
// This ensures child bindings are evaluated before handler creation
|
||||
PropagateBindingContext(view, item);
|
||||
|
||||
// Create handler for the view
|
||||
if (view.Handler == null && handler.MauiContext != null)
|
||||
{
|
||||
view.Handler = view.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
else if (content is ViewCell cell)
|
||||
{
|
||||
cell.BindingContext = item;
|
||||
var cellView = cell.View;
|
||||
if (cellView != null)
|
||||
{
|
||||
if (cellView.Handler == null && handler.MauiContext != null)
|
||||
{
|
||||
cellView.Handler = cellView.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (cellView.Handler?.PlatformView is SkiaView skiaView)
|
||||
{
|
||||
return skiaView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore template creation errors
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView)
|
||||
@@ -146,19 +267,40 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
|
||||
public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
|
||||
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
|
||||
|
||||
try
|
||||
{
|
||||
handler._isUpdatingSelection = true;
|
||||
if (!Equals(handler.PlatformView.SelectedItem, collectionView.SelectedItem))
|
||||
{
|
||||
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
handler._isUpdatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
|
||||
|
||||
// Sync selected items
|
||||
var selectedItems = collectionView.SelectedItems;
|
||||
if (selectedItems != null && selectedItems.Count > 0)
|
||||
try
|
||||
{
|
||||
handler.PlatformView.SelectedItem = selectedItems.First();
|
||||
handler._isUpdatingSelection = true;
|
||||
|
||||
// Sync selected items
|
||||
var selectedItems = collectionView.SelectedItems;
|
||||
if (selectedItems != null && selectedItems.Count > 0)
|
||||
{
|
||||
handler.PlatformView.SelectedItem = selectedItems.First();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
handler._isUpdatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,9 +356,23 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// Don't override if BackgroundColor is explicitly set
|
||||
if (collectionView.BackgroundColor is not null)
|
||||
return;
|
||||
|
||||
if (collectionView.Background is SolidColorBrush solidBrush)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(CollectionViewHandler handler, CollectionView collectionView)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (collectionView.BackgroundColor is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,4 +390,32 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively propagates binding context to all child views to force binding evaluation.
|
||||
/// </summary>
|
||||
private static void PropagateBindingContext(View view, object? bindingContext)
|
||||
{
|
||||
view.BindingContext = bindingContext;
|
||||
|
||||
// Propagate to children
|
||||
if (view is Layout layout)
|
||||
{
|
||||
foreach (var child in layout.Children)
|
||||
{
|
||||
if (child is View childView)
|
||||
{
|
||||
PropagateBindingContext(childView, bindingContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (view is ContentView contentView && contentView.Content != null)
|
||||
{
|
||||
PropagateBindingContext(contentView.Content, bindingContext);
|
||||
}
|
||||
else if (view is Border border && border.Content is View borderContent)
|
||||
{
|
||||
PropagateBindingContext(borderContent, bindingContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
[nameof(IDatePicker.Format)] = MapFormat,
|
||||
[nameof(IDatePicker.TextColor)] = MapTextColor,
|
||||
[nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
};
|
||||
|
||||
@@ -49,6 +50,17 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
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)
|
||||
@@ -57,11 +69,11 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnDateSelected(object? sender, EventArgs e)
|
||||
private void OnDateSelected(object? sender, DateChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
|
||||
VirtualView.Date = PlatformView.Date;
|
||||
VirtualView.Date = e.NewDate;
|
||||
}
|
||||
|
||||
public static void MapDate(DatePickerHandler handler, IDatePicker datePicker)
|
||||
@@ -93,13 +105,33 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
if (handler.PlatformView is null) return;
|
||||
if (datePicker.TextColor is not null)
|
||||
{
|
||||
handler.PlatformView.TextColor = datePicker.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = datePicker.TextColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapCharacterSpacing(DatePickerHandler handler, IDatePicker datePicker)
|
||||
{
|
||||
// Character spacing would require custom text rendering
|
||||
if (handler.PlatformView is null) return;
|
||||
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)
|
||||
@@ -108,7 +140,7 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
|
||||
if (datePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
[nameof(IEditor.Placeholder)] = MapPlaceholder,
|
||||
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
|
||||
[nameof(IEditor.TextColor)] = MapTextColor,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
|
||||
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
|
||||
[nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
|
||||
[nameof(IEditor.MaxLength)] = MapMaxLength,
|
||||
[nameof(IEditor.CursorPosition)] = MapCursorPosition,
|
||||
[nameof(IEditor.SelectionLength)] = MapSelectionLength,
|
||||
@@ -31,6 +33,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
[nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public static CommandMapper<IEditor, EditorHandler> CommandMapper =
|
||||
@@ -82,6 +85,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.Text = editor.Text ?? "";
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapPlaceholder(EditorHandler handler, IEditor editor)
|
||||
@@ -95,7 +99,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
if (handler.PlatformView is null) return;
|
||||
if (editor.PlaceholderColor is not null)
|
||||
{
|
||||
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor();
|
||||
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,13 +108,34 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
if (handler.PlatformView is null) return;
|
||||
if (editor.TextColor is not null)
|
||||
{
|
||||
handler.PlatformView.TextColor = editor.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = editor.TextColor;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// Character spacing would require custom text rendering
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.CharacterSpacing = editor.CharacterSpacing;
|
||||
}
|
||||
|
||||
public static void MapIsReadOnly(EditorHandler handler, IEditor editor)
|
||||
@@ -121,7 +146,14 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
|
||||
public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor)
|
||||
{
|
||||
// Text prediction not applicable to desktop
|
||||
if (handler.PlatformView is null) return;
|
||||
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)
|
||||
@@ -138,22 +170,39 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
|
||||
public static void MapSelectionLength(EditorHandler handler, IEditor editor)
|
||||
{
|
||||
// Selection would need to be added to SkiaEditor
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.SelectionLength = editor.SelectionLength;
|
||||
}
|
||||
|
||||
public static void MapKeyboard(EditorHandler handler, IEditor editor)
|
||||
{
|
||||
// Virtual keyboard type not applicable to desktop
|
||||
// Virtual keyboard type not applicable to desktop - stored for future use
|
||||
}
|
||||
|
||||
public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor)
|
||||
{
|
||||
// Text alignment would require changes to SkiaEditor drawing
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
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)
|
||||
{
|
||||
// Text alignment would require changes to SkiaEditor drawing
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
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)
|
||||
@@ -162,7 +211,18 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
||||
|
||||
if (editor.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.EditorBackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(EditorHandler handler, IEditor editor)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (editor is Editor ve && ve.BackgroundColor != null)
|
||||
{
|
||||
handler.PlatformView.EditorBackgroundColor = ve.BackgroundColor;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +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 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,
|
||||
};
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -28,9 +29,13 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
[nameof(IEntry.IsPassword)] = MapIsPassword,
|
||||
[nameof(IEntry.ReturnType)] = MapReturnType,
|
||||
[nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility,
|
||||
[nameof(IEntry.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
|
||||
[nameof(IEntry.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
|
||||
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick,
|
||||
};
|
||||
|
||||
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -65,13 +70,23 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private bool _isUpdatingText;
|
||||
|
||||
private void OnTextChanged(object? sender, Platform.TextChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
if (VirtualView is null || PlatformView is null || _isUpdatingText) return;
|
||||
|
||||
if (VirtualView.Text != e.NewTextValue)
|
||||
{
|
||||
VirtualView.Text = e.NewTextValue ?? string.Empty;
|
||||
_isUpdatingText = true;
|
||||
try
|
||||
{
|
||||
VirtualView.Text = e.NewTextValue ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingText = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +97,13 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
|
||||
public static void MapText(EntryHandler handler, IEntry entry)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
if (handler.PlatformView is null || handler._isUpdatingText) return;
|
||||
|
||||
if (handler.PlatformView.Text != entry.Text)
|
||||
{
|
||||
handler.PlatformView.Text = entry.Text ?? string.Empty;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapTextColor(EntryHandler handler, IEntry entry)
|
||||
@@ -93,7 +111,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (entry.TextColor is not null)
|
||||
handler.PlatformView.TextColor = entry.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = entry.TextColor;
|
||||
}
|
||||
|
||||
public static void MapFont(EntryHandler handler, IEntry entry)
|
||||
@@ -102,19 +120,24 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
|
||||
var font = entry.Font;
|
||||
if (font.Size > 0)
|
||||
handler.PlatformView.FontSize = (float)font.Size;
|
||||
handler.PlatformView.FontSize = font.Size;
|
||||
|
||||
if (!string.IsNullOrEmpty(font.Family))
|
||||
handler.PlatformView.FontFamily = font.Family;
|
||||
|
||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
||||
// 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(EntryHandler handler, IEntry entry)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.CharacterSpacing = (float)entry.CharacterSpacing;
|
||||
handler.PlatformView.CharacterSpacing = entry.CharacterSpacing;
|
||||
}
|
||||
|
||||
public static void MapPlaceholder(EntryHandler handler, IEntry entry)
|
||||
@@ -128,7 +151,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (entry.PlaceholderColor is not null)
|
||||
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor();
|
||||
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor;
|
||||
}
|
||||
|
||||
public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
|
||||
@@ -174,16 +197,28 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
|
||||
{
|
||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
||||
_ => Platform.TextAlignment.Start
|
||||
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||
_ => TextAlignment.Start
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,10 +228,10 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
|
||||
handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch
|
||||
{
|
||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
||||
_ => Platform.TextAlignment.Center
|
||||
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||
_ => TextAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,7 +241,29 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
|
||||
if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
Handlers/FlexLayoutHandler.cs
Normal file
105
Handlers/FlexLayoutHandler.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// 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;
|
||||
@@ -13,12 +15,17 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
/// </summary>
|
||||
public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage>
|
||||
{
|
||||
private bool _isUpdatingPresented;
|
||||
|
||||
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.FlyoutWidth)] = MapFlyoutWidth,
|
||||
[nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled,
|
||||
[nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
};
|
||||
|
||||
public static CommandMapper<IFlyoutView, FlyoutPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -55,13 +62,82 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
|
||||
|
||||
private void OnIsPresentedChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Sync back to the virtual view
|
||||
if (VirtualView is null || PlatformView is null || _isUpdatingPresented) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isUpdatingPresented = true;
|
||||
// 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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsPresented = flyoutView.IsPresented;
|
||||
if (handler.PlatformView is null || handler._isUpdatingPresented) return;
|
||||
|
||||
try
|
||||
{
|
||||
handler._isUpdatingPresented = true;
|
||||
handler.PlatformView.IsPresented = flyoutView.IsPresented;
|
||||
}
|
||||
finally
|
||||
{
|
||||
handler._isUpdatingPresented = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||
@@ -88,4 +164,14 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
|
||||
_ => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
Handlers/FrameHandler.cs
Normal file
122
Handlers/FrameHandler.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for Frame on Linux using SkiaFrame.
|
||||
/// </summary>
|
||||
public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
|
||||
{
|
||||
public static IPropertyMapper<Frame, FrameHandler> Mapper =
|
||||
new PropertyMapper<Frame, FrameHandler>(ViewMapper)
|
||||
{
|
||||
[nameof(Frame.BorderColor)] = MapBorderColor,
|
||||
[nameof(Frame.CornerRadius)] = MapCornerRadius,
|
||||
[nameof(Frame.HasShadow)] = MapHasShadow,
|
||||
[nameof(Frame.BackgroundColor)] = MapBackgroundColor,
|
||||
[nameof(Frame.Padding)] = MapPadding,
|
||||
[nameof(Frame.Content)] = MapContent,
|
||||
};
|
||||
|
||||
public FrameHandler() : base(Mapper)
|
||||
{
|
||||
}
|
||||
|
||||
public FrameHandler(IPropertyMapper? mapper)
|
||||
: base(mapper ?? Mapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SkiaFrame CreatePlatformView()
|
||||
{
|
||||
return new SkiaFrame();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (frame.BorderColor != null)
|
||||
{
|
||||
handler.PlatformView.Stroke = frame.BorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapCornerRadius(FrameHandler handler, Frame frame)
|
||||
{
|
||||
handler.PlatformView.CornerRadius = frame.CornerRadius;
|
||||
}
|
||||
|
||||
public static void MapHasShadow(FrameHandler handler, Frame frame)
|
||||
{
|
||||
handler.PlatformView.HasShadow = frame.HasShadow;
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(FrameHandler handler, Frame frame)
|
||||
{
|
||||
if (frame.BackgroundColor != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = frame.BackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapPadding(FrameHandler handler, Frame frame)
|
||||
{
|
||||
handler.PlatformView.SetPadding(
|
||||
(float)frame.Padding.Left,
|
||||
(float)frame.Padding.Top,
|
||||
(float)frame.Padding.Right,
|
||||
(float)frame.Padding.Bottom);
|
||||
}
|
||||
|
||||
public static void MapContent(FrameHandler handler, Frame frame)
|
||||
{
|
||||
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||
|
||||
handler.PlatformView.ClearChildren();
|
||||
|
||||
var content = frame.Content;
|
||||
if (content != null)
|
||||
{
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
handler.PlatformView.AddChild(skiaContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
856
Handlers/GestureManager.cs
Normal file
856
Handlers/GestureManager.cs
Normal file
@@ -0,0 +1,856 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public partial class GraphicsViewHandler : ViewHandler<IGraphicsView, SkiaGraphi
|
||||
|
||||
if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
265
Handlers/GtkWebViewHandler.cs
Normal file
265
Handlers/GtkWebViewHandler.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
60
Handlers/GtkWebViewManager.cs
Normal file
60
Handlers/GtkWebViewManager.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
545
Handlers/GtkWebViewPlatformView.cs
Normal file
545
Handlers/GtkWebViewPlatformView.cs
Normal file
@@ -0,0 +1,545 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Handlers/GtkWebViewProxy.cs
Normal file
71
Handlers/GtkWebViewProxy.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
||||
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
[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)
|
||||
@@ -118,13 +123,13 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (imageButton.StrokeColor is not null)
|
||||
handler.PlatformView.StrokeColor = imageButton.StrokeColor.ToSKColor();
|
||||
handler.PlatformView.StrokeColor = imageButton.StrokeColor;
|
||||
}
|
||||
|
||||
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.StrokeThickness = (float)imageButton.StrokeThickness;
|
||||
handler.PlatformView.StrokeThickness = imageButton.StrokeThickness;
|
||||
}
|
||||
|
||||
public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton)
|
||||
@@ -136,12 +141,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
||||
public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
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;
|
||||
handler.PlatformView.Padding = imageButton.Padding;
|
||||
}
|
||||
|
||||
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
|
||||
@@ -150,7 +150,59 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
||||
|
||||
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.ImageBackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -20,6 +23,10 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
[nameof(IImage.IsOpaque)] = MapIsOpaque,
|
||||
[nameof(IImageSourcePart.Source)] = MapSource,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["Width"] = MapWidth,
|
||||
["Height"] = MapHeight,
|
||||
["HorizontalOptions"] = MapHorizontalOptions,
|
||||
["VerticalOptions"] = MapVerticalOptions,
|
||||
};
|
||||
|
||||
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -88,6 +95,19 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -97,7 +117,57 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
|
||||
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.ImageBackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +232,14 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
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)
|
||||
{
|
||||
@@ -176,5 +254,73 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
157
Handlers/IndicatorViewHandler.cs
Normal file
157
Handlers/IndicatorViewHandler.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, Skia
|
||||
|
||||
if (itemsView.Background is SolidColorBrush solidBrush)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,154 +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 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,
|
||||
};
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -23,8 +26,13 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
|
||||
[nameof(ILabel.LineHeight)] = MapLineHeight,
|
||||
["LineBreakMode"] = MapLineBreakMode,
|
||||
["MaxLines"] = MapMaxLines,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||
["FormattedText"] = MapFormattedText,
|
||||
};
|
||||
|
||||
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -45,6 +53,45 @@ public partial class LabelHandler : ViewHandler<ILabel, 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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -56,7 +103,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (label.TextColor is not null)
|
||||
handler.PlatformView.TextColor = label.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = label.TextColor;
|
||||
}
|
||||
|
||||
public static void MapFont(LabelHandler handler, ILabel label)
|
||||
@@ -65,32 +112,37 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
|
||||
var font = label.Font;
|
||||
if (font.Size > 0)
|
||||
handler.PlatformView.FontSize = (float)font.Size;
|
||||
handler.PlatformView.FontSize = font.Size;
|
||||
|
||||
if (!string.IsNullOrEmpty(font.Family))
|
||||
handler.PlatformView.FontFamily = font.Family;
|
||||
|
||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
||||
// 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(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.CharacterSpacing = (float)label.CharacterSpacing;
|
||||
handler.PlatformView.CharacterSpacing = label.CharacterSpacing;
|
||||
}
|
||||
|
||||
public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// Map MAUI TextAlignment to our internal TextAlignment
|
||||
// Map MAUI TextAlignment to our TextAlignment
|
||||
handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch
|
||||
{
|
||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
||||
_ => Platform.TextAlignment.Start
|
||||
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||
_ => TextAlignment.Start
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,25 +152,45 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
|
||||
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
|
||||
{
|
||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
||||
_ => Platform.TextAlignment.Center
|
||||
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||
_ => TextAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapTextDecorations(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0;
|
||||
handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0;
|
||||
handler.PlatformView.TextDecorations = label.TextDecorations;
|
||||
}
|
||||
|
||||
public static void MapLineHeight(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.LineHeight = (float)label.LineHeight;
|
||||
handler.PlatformView.LineHeight = label.LineHeight;
|
||||
}
|
||||
|
||||
public static void MapLineBreakMode(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// LineBreakMode is on Label control, not ILabel interface
|
||||
if (label is Microsoft.Maui.Controls.Label mauiLabel)
|
||||
{
|
||||
handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapMaxLines(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// MaxLines is on Label control, not ILabel interface
|
||||
if (label is Microsoft.Maui.Controls.Label mauiLabel)
|
||||
{
|
||||
handler.PlatformView.MaxLines = mauiLabel.MaxLines;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapPadding(LabelHandler handler, ILabel label)
|
||||
@@ -126,11 +198,11 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
var padding = label.Padding;
|
||||
handler.PlatformView.Padding = new SKRect(
|
||||
(float)padding.Left,
|
||||
(float)padding.Top,
|
||||
(float)padding.Right,
|
||||
(float)padding.Bottom);
|
||||
handler.PlatformView.Padding = new Thickness(
|
||||
padding.Left,
|
||||
padding.Top,
|
||||
padding.Right,
|
||||
padding.Bottom);
|
||||
}
|
||||
|
||||
public static void MapBackground(LabelHandler handler, ILabel label)
|
||||
@@ -139,7 +211,48 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
|
||||
if (label.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.VerticalOptions = label.VerticalLayoutAlignment switch
|
||||
{
|
||||
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
|
||||
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
|
||||
Primitives.LayoutAlignment.End => LayoutOptions.End,
|
||||
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
|
||||
_ => LayoutOptions.Start
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.HorizontalOptions = label.HorizontalLayoutAlignment switch
|
||||
{
|
||||
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
|
||||
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
|
||||
Primitives.LayoutAlignment.End => LayoutOptions.End,
|
||||
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
|
||||
_ => LayoutOptions.Start
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -17,7 +19,9 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
public static IPropertyMapper<ILayout, LayoutHandler> Mapper = new PropertyMapper<ILayout, LayoutHandler>(ViewHandler.ViewMapper)
|
||||
{
|
||||
[nameof(ILayout.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -53,22 +57,84 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
return new SkiaStackLayout();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Explicitly map BackgroundColor since it may be set before handler creation
|
||||
// (e.g., in ItemTemplates for CollectionView)
|
||||
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
{
|
||||
platformView.BackgroundColor = ve.BackgroundColor;
|
||||
platformView.Invalidate();
|
||||
}
|
||||
|
||||
// Add existing children (important for template-created views)
|
||||
if (VirtualView is ILayout layout && MauiContext != null)
|
||||
{
|
||||
for (int i = 0; i < layout.Count; i++)
|
||||
{
|
||||
var child = layout[i];
|
||||
if (child == null) continue;
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
{
|
||||
platformView.AddChild(skiaChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackground(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
// Don't override if BackgroundColor is explicitly set
|
||||
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
return;
|
||||
|
||||
var background = layout.Background;
|
||||
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
}
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
handler.PlatformView.ClipToBounds = layout.ClipsToBounds;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapPadding(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
if (layout is IPadding paddable)
|
||||
{
|
||||
var padding = paddable.Padding;
|
||||
handler.PlatformView.Padding = new SKRect(
|
||||
(float)padding.Left,
|
||||
(float)padding.Top,
|
||||
(float)padding.Right,
|
||||
(float)padding.Bottom);
|
||||
handler.PlatformView.InvalidateMeasure();
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg)
|
||||
{
|
||||
if (arg is LayoutHandlerUpdate update)
|
||||
@@ -194,9 +260,16 @@ public partial class GridHandler : LayoutHandler
|
||||
{
|
||||
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
||||
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
|
||||
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
|
||||
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
|
||||
};
|
||||
|
||||
public GridHandler() : base(Mapper)
|
||||
public static new CommandMapper<IGridLayout, GridHandler> GridCommandMapper = new(LayoutHandler.CommandMapper)
|
||||
{
|
||||
["Add"] = MapGridAdd,
|
||||
};
|
||||
|
||||
public GridHandler() : base(Mapper, GridCommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -205,6 +278,52 @@ public partial class GridHandler : LayoutHandler
|
||||
return new SkiaGrid();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Map definitions on connect
|
||||
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
|
||||
{
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
|
||||
UpdateRowDefinitions(grid, gridLayout);
|
||||
UpdateColumnDefinitions(grid, gridLayout);
|
||||
|
||||
// Add existing children (important for template-created views)
|
||||
for (int i = 0; i < gridLayout.Count; i++)
|
||||
{
|
||||
var child = gridLayout[i];
|
||||
if (child == null) continue;
|
||||
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
|
||||
}
|
||||
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
{
|
||||
// Get grid position from attached properties
|
||||
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
|
||||
if (child is Microsoft.Maui.Controls.View mauiView)
|
||||
{
|
||||
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
|
||||
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
|
||||
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
|
||||
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
|
||||
}
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Adding child[{i}] at row={row}, col={column}");
|
||||
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
|
||||
}
|
||||
}
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid now has {grid.Children.Count} SkiaView children");
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapColumnSpacing(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is SkiaGrid grid)
|
||||
@@ -222,6 +341,79 @@ public partial class GridHandler : LayoutHandler
|
||||
grid.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is SkiaGrid grid)
|
||||
{
|
||||
UpdateRowDefinitions(grid, layout);
|
||||
grid.InvalidateMeasure();
|
||||
grid.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is SkiaGrid grid)
|
||||
{
|
||||
UpdateColumnDefinitions(grid, layout);
|
||||
grid.InvalidateMeasure();
|
||||
grid.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateRowDefinitions(SkiaGrid grid, IGridLayout layout)
|
||||
{
|
||||
grid.RowDefinitions.Clear();
|
||||
foreach (var rowDef in layout.RowDefinitions)
|
||||
{
|
||||
var height = rowDef.Height;
|
||||
if (height.IsAbsolute)
|
||||
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Absolute));
|
||||
else if (height.IsAuto)
|
||||
grid.RowDefinitions.Add(GridLength.Auto);
|
||||
else // Star
|
||||
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Star));
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateColumnDefinitions(SkiaGrid grid, IGridLayout layout)
|
||||
{
|
||||
grid.ColumnDefinitions.Clear();
|
||||
foreach (var colDef in layout.ColumnDefinitions)
|
||||
{
|
||||
var width = colDef.Width;
|
||||
if (width.IsAbsolute)
|
||||
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Absolute));
|
||||
else if (width.IsAuto)
|
||||
grid.ColumnDefinitions.Add(GridLength.Auto);
|
||||
else // Star
|
||||
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Star));
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapGridAdd(GridHandler handler, ILayout layout, object? arg)
|
||||
{
|
||||
if (arg is LayoutHandlerUpdate update && handler.PlatformView is SkiaGrid grid)
|
||||
{
|
||||
var childHandler = update.View.Handler;
|
||||
if (childHandler?.PlatformView is SkiaView skiaView)
|
||||
{
|
||||
// Get grid position from attached properties
|
||||
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
|
||||
|
||||
if (update.View is Microsoft.Maui.Controls.View mauiView)
|
||||
{
|
||||
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
|
||||
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
|
||||
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
|
||||
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
|
||||
}
|
||||
|
||||
grid.AddChild(skiaView, row, column, rowSpan, columnSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -17,6 +19,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
{
|
||||
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IPadding.Padding)] = MapPadding,
|
||||
};
|
||||
|
||||
public static CommandMapper<ILayout, LayoutHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -42,6 +45,38 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
return new SkiaStackLayout();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Create handlers for all children and add them to the platform view
|
||||
if (VirtualView == null || MauiContext == null) return;
|
||||
|
||||
// Explicitly map BackgroundColor since it may be set before handler creation
|
||||
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
{
|
||||
platformView.BackgroundColor = ve.BackgroundColor;
|
||||
}
|
||||
|
||||
for (int i = 0; i < VirtualView.Count; i++)
|
||||
{
|
||||
var child = VirtualView[i];
|
||||
if (child == null) continue;
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
// Add child's platform view to our layout
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
{
|
||||
platformView.AddChild(skiaChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
if (handler.PlatformView == null) return;
|
||||
@@ -54,7 +89,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
|
||||
if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +137,18 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
// Force re-layout
|
||||
handler.PlatformView?.InvalidateMeasure();
|
||||
}
|
||||
|
||||
public static void MapPadding(LayoutHandler handler, ILayout layout)
|
||||
{
|
||||
if (handler.PlatformView == null) return;
|
||||
|
||||
if (layout is IPadding paddable)
|
||||
{
|
||||
handler.PlatformView.Padding = paddable.Padding;
|
||||
handler.PlatformView.InvalidateMeasure();
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -138,6 +185,29 @@ public partial class StackLayoutHandler : LayoutHandler
|
||||
return new SkiaStackLayout();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
// Set orientation first
|
||||
if (platformView is SkiaStackLayout stackLayout && VirtualView is IStackLayout stackView)
|
||||
{
|
||||
// Determine orientation based on view type
|
||||
if (VirtualView is Microsoft.Maui.Controls.HorizontalStackLayout)
|
||||
{
|
||||
stackLayout.Orientation = StackOrientation.Horizontal;
|
||||
}
|
||||
else if (VirtualView is Microsoft.Maui.Controls.VerticalStackLayout ||
|
||||
VirtualView is Microsoft.Maui.Controls.StackLayout)
|
||||
{
|
||||
stackLayout.Orientation = StackOrientation.Vertical;
|
||||
}
|
||||
|
||||
stackLayout.Spacing = (float)stackView.Spacing;
|
||||
}
|
||||
|
||||
// Let base handle children
|
||||
base.ConnectHandler(platformView);
|
||||
}
|
||||
|
||||
public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is SkiaStackLayout stackLayout)
|
||||
@@ -156,6 +226,8 @@ public partial class GridHandler : LayoutHandler
|
||||
{
|
||||
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
|
||||
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
||||
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
|
||||
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
|
||||
};
|
||||
|
||||
public GridHandler() : base(Mapper)
|
||||
@@ -167,6 +239,75 @@ public partial class GridHandler : LayoutHandler
|
||||
return new SkiaGrid();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Don't call base - we handle children specially for Grid
|
||||
if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return;
|
||||
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols");
|
||||
|
||||
// Explicitly map BackgroundColor since it may be set before handler creation
|
||||
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
{
|
||||
platformView.BackgroundColor = ve.BackgroundColor;
|
||||
}
|
||||
|
||||
// Explicitly map Padding since it may be set before handler creation
|
||||
if (VirtualView is IPadding paddable)
|
||||
{
|
||||
var padding = paddable.Padding;
|
||||
platformView.Padding = padding;
|
||||
DiagnosticLog.Debug("GridHandler", $"Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
|
||||
}
|
||||
|
||||
// Map row/column definitions first
|
||||
MapRowDefinitions(this, gridLayout);
|
||||
MapColumnDefinitions(this, gridLayout);
|
||||
|
||||
// Add each child with its row/column position
|
||||
for (int i = 0; i < gridLayout.Count; i++)
|
||||
{
|
||||
var child = gridLayout[i];
|
||||
if (child == null) continue;
|
||||
|
||||
DiagnosticLog.Debug("GridHandler", $"Processing child {i}: {child.GetType().Name}");
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
// Get grid position from attached properties
|
||||
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
|
||||
if (child is Microsoft.Maui.Controls.View mauiView)
|
||||
{
|
||||
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
|
||||
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
|
||||
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
|
||||
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
|
||||
}
|
||||
|
||||
DiagnosticLog.Debug("GridHandler", $"Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
|
||||
|
||||
// Add child's platform view to our grid
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
{
|
||||
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
|
||||
DiagnosticLog.Debug("GridHandler", $"Added child {i} to grid");
|
||||
}
|
||||
}
|
||||
DiagnosticLog.Debug("GridHandler", "ConnectHandler complete");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("GridHandler", $"EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapRowSpacing(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is SkiaGrid grid)
|
||||
@@ -182,4 +323,38 @@ public partial class GridHandler : LayoutHandler
|
||||
grid.ColumnSpacing = (float)layout.ColumnSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is not SkiaGrid grid) return;
|
||||
|
||||
grid.RowDefinitions.Clear();
|
||||
foreach (var rowDef in layout.RowDefinitions)
|
||||
{
|
||||
var height = rowDef.Height;
|
||||
if (height.IsAbsolute)
|
||||
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
|
||||
else if (height.IsAuto)
|
||||
grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
|
||||
else // Star
|
||||
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Star));
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
|
||||
{
|
||||
if (handler.PlatformView is not SkiaGrid grid) return;
|
||||
|
||||
grid.ColumnDefinitions.Clear();
|
||||
foreach (var colDef in layout.ColumnDefinitions)
|
||||
{
|
||||
var width = colDef.Width;
|
||||
if (width.IsAbsolute)
|
||||
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
|
||||
else if (width.IsAuto)
|
||||
grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
|
||||
else // Star
|
||||
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Star));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
376
Handlers/MenuBarHandler.cs
Normal file
376
Handlers/MenuBarHandler.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Svg.Skia;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -50,10 +55,15 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
platformView.Popped += OnPopped;
|
||||
platformView.PoppedToRoot += OnPoppedToRoot;
|
||||
|
||||
// Set initial root page if exists
|
||||
if (VirtualView.CurrentPage != null)
|
||||
// Subscribe to navigation events from virtual view
|
||||
if (VirtualView != null)
|
||||
{
|
||||
SetupInitialPage();
|
||||
VirtualView.Pushed += OnVirtualViewPushed;
|
||||
VirtualView.Popped += OnVirtualViewPopped;
|
||||
VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot;
|
||||
|
||||
// Set up initial navigation stack
|
||||
SetupNavigationStack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,16 +72,270 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
platformView.Pushed -= OnPushed;
|
||||
platformView.Popped -= OnPopped;
|
||||
platformView.PoppedToRoot -= OnPoppedToRoot;
|
||||
|
||||
if (VirtualView != null)
|
||||
{
|
||||
VirtualView.Pushed -= OnVirtualViewPushed;
|
||||
VirtualView.Popped -= OnVirtualViewPopped;
|
||||
VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot;
|
||||
}
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void SetupInitialPage()
|
||||
private void SetupNavigationStack()
|
||||
{
|
||||
var currentPage = VirtualView.CurrentPage;
|
||||
if (currentPage?.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
if (VirtualView == null || PlatformView == null || MauiContext == null) return;
|
||||
|
||||
// Get all pages in the navigation stack
|
||||
var pages = VirtualView.Navigation.NavigationStack.ToList();
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up {pages.Count} pages");
|
||||
|
||||
// If no pages in stack, check CurrentPage
|
||||
if (pages.Count == 0 && VirtualView.CurrentPage != null)
|
||||
{
|
||||
PlatformView.SetRootPage(skiaPage);
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
|
||||
pages.Add(VirtualView.CurrentPage);
|
||||
}
|
||||
|
||||
foreach (var page in pages)
|
||||
{
|
||||
// Ensure the page has a handler
|
||||
if (page.Handler == null)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for: {page.Title}");
|
||||
page.Handler = page.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Page handler type: {page.Handler?.GetType().Name}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
|
||||
|
||||
if (page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
{
|
||||
// Set navigation bar properties
|
||||
skiaPage.ShowNavigationBar = true;
|
||||
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
|
||||
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||
skiaPage.Title = page.Title ?? "";
|
||||
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||
|
||||
// If content is null, try to get it from ContentPage
|
||||
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, manually 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}");
|
||||
}
|
||||
}
|
||||
|
||||
// Map toolbar items
|
||||
MapToolbarItems(skiaPage, page);
|
||||
|
||||
if (PlatformView.StackDepth == 0)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Setting root page: {page.Title}");
|
||||
PlatformView.SetRootPage(skiaPage);
|
||||
}
|
||||
else
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Pushing page: {page.Title}");
|
||||
PlatformView.Push(skiaPage, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DiagnosticLog.Warn("NavigationPageHandler", $"Failed to get SkiaPage for: {page.Title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<Page, (SkiaPage, INotifyCollectionChanged)> _toolbarSubscriptions = new();
|
||||
|
||||
private void MapToolbarItems(SkiaPage skiaPage, Page page)
|
||||
{
|
||||
if (skiaPage is SkiaContentPage contentPage)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
|
||||
|
||||
contentPage.ToolbarItems.Clear();
|
||||
foreach (var item in page.ToolbarItems)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Adding toolbar item: '{item.Text}', IconImageSource={item.IconImageSource}, Order={item.Order}");
|
||||
// Default and Primary should both be treated as Primary (shown in toolbar)
|
||||
// Only Secondary goes to overflow menu
|
||||
var order = item.Order == ToolbarItemOrder.Secondary
|
||||
? SkiaToolbarItemOrder.Secondary
|
||||
: SkiaToolbarItemOrder.Primary;
|
||||
|
||||
// Create a command that invokes the Clicked event
|
||||
var toolbarItem = item; // Capture for closure
|
||||
var clickCommand = new RelayCommand(() =>
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
|
||||
// Use IMenuItemController to send the click
|
||||
if (toolbarItem is IMenuItemController menuController)
|
||||
{
|
||||
menuController.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: invoke Command if set
|
||||
toolbarItem.Command?.Execute(toolbarItem.CommandParameter);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
{
|
||||
Text = item.Text ?? "",
|
||||
Icon = icon,
|
||||
Order = order,
|
||||
Command = clickCommand
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to ToolbarItems changes if not already subscribed
|
||||
if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page))
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Subscribing to ToolbarItems changes for '{page.Title}'");
|
||||
notifyCollection.CollectionChanged += (s, e) =>
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItems changed for '{page.Title}', action={e.Action}");
|
||||
MapToolbarItems(skiaPage, page);
|
||||
skiaPage.Invalidate();
|
||||
};
|
||||
_toolbarSubscriptions[page] = (skiaPage, notifyCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Pushed: {e.Page?.Title}");
|
||||
if (e.Page == null || PlatformView == null || MauiContext == null) return;
|
||||
|
||||
// Ensure the page has a handler
|
||||
if (e.Page.Handler == null)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for page: {e.Page.GetType().Name}");
|
||||
e.Page.Handler = e.Page.ToViewHandler(MauiContext);
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Handler created: {e.Page.Handler?.GetType().Name}");
|
||||
}
|
||||
|
||||
if (e.Page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||
skiaPage.ShowNavigationBar = true;
|
||||
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
|
||||
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||
skiaPage.Title = e.Page.Title ?? "";
|
||||
|
||||
// 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);
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "Pushing page to platform");
|
||||
PlatformView.Push(skiaPage, false);
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Push complete, thread={Environment.CurrentManagedThreadId}");
|
||||
}
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "OnVirtualViewPushed returning");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("NavigationPageHandler", $"EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Popped: {e.Page?.Title}");
|
||||
// Pop on the platform side to sync with MAUI navigation
|
||||
PlatformView?.Pop();
|
||||
}
|
||||
|
||||
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "VirtualView PoppedToRoot");
|
||||
PlatformView?.PopToRoot();
|
||||
}
|
||||
|
||||
private void OnPushed(object? sender, NavigationEventArgs e)
|
||||
@@ -81,7 +345,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
private void OnPopped(object? sender, NavigationEventArgs e)
|
||||
{
|
||||
// Sync back to virtual view if needed
|
||||
// Sync back to virtual view - pop from MAUI navigation stack
|
||||
if (VirtualView?.Navigation.NavigationStack.Count > 1)
|
||||
{
|
||||
// Don't trigger another pop on platform side
|
||||
VirtualView.Navigation.RemovePage(VirtualView.Navigation.NavigationStack.Last());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPoppedToRoot(object? sender, NavigationEventArgs e)
|
||||
@@ -95,7 +364,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
if (navigationPage.BarBackgroundColor is not null)
|
||||
{
|
||||
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor();
|
||||
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +374,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
if (navigationPage.BarBackground is SolidColorBrush solidBrush)
|
||||
{
|
||||
handler.PlatformView.BarBackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BarBackgroundColor = solidBrush.Color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +384,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
if (navigationPage.BarTextColor is not null)
|
||||
{
|
||||
handler.PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor();
|
||||
handler.PlatformView.BarTextColor = navigationPage.BarTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,20 +394,35 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
if (navigationPage.Background is SolidColorBrush solidBrush)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapRequestNavigation(NavigationPageHandler handler, NavigationPage navigationPage, object? args)
|
||||
{
|
||||
if (handler.PlatformView is null || args is not NavigationRequest request)
|
||||
if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request)
|
||||
return;
|
||||
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"MapRequestNavigation: {request.NavigationStack.Count} pages");
|
||||
|
||||
// Handle navigation request
|
||||
foreach (var page in request.NavigationStack)
|
||||
foreach (var view in request.NavigationStack)
|
||||
{
|
||||
if (view is not Page page) continue;
|
||||
|
||||
// Ensure handler exists
|
||||
if (page.Handler == null)
|
||||
{
|
||||
page.Handler = page.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
{
|
||||
skiaPage.ShowNavigationBar = true;
|
||||
skiaPage.TitleBarColor = handler.PlatformView.BarBackgroundColor;
|
||||
skiaPage.TitleTextColor = handler.PlatformView.BarTextColor;
|
||||
handler.MapToolbarItems(skiaPage, page);
|
||||
|
||||
if (handler.PlatformView.StackDepth == 0)
|
||||
{
|
||||
handler.PlatformView.SetRootPage(skiaPage);
|
||||
@@ -151,3 +435,26 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple relay command for invoking actions.
|
||||
/// </summary>
|
||||
internal class RelayCommand : System.Windows.Input.ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -19,8 +21,11 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
{
|
||||
[nameof(Page.Title)] = MapTitle,
|
||||
[nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource,
|
||||
[nameof(Page.IconImageSource)] = MapIconImageSource,
|
||||
[nameof(Page.Padding)] = MapPadding,
|
||||
[nameof(Page.IsBusy)] = MapIsBusy,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(VisualElement.BackgroundColor)] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public static CommandMapper<Page, PageHandler> CommandMapper =
|
||||
@@ -45,6 +50,10 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
protected override void ConnectHandler(SkiaPage platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Set MauiPage reference for theme refresh support
|
||||
platformView.MauiPage = VirtualView;
|
||||
|
||||
platformView.Appearing += OnAppearing;
|
||||
platformView.Disappearing += OnDisappearing;
|
||||
}
|
||||
@@ -53,11 +62,13 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
{
|
||||
platformView.Appearing -= OnAppearing;
|
||||
platformView.Disappearing -= OnDisappearing;
|
||||
platformView.MauiPage = null;
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnAppearing(object? sender, EventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("PageHandler", $"OnAppearing received for: {VirtualView?.Title}");
|
||||
(VirtualView as IPageController)?.SendAppearing();
|
||||
}
|
||||
|
||||
@@ -96,9 +107,34 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
|
||||
if (page.Background is SolidColorBrush solidBrush)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -110,6 +146,7 @@ public partial class ContentPageHandler : PageHandler
|
||||
new PropertyMapper<ContentPage, ContentPageHandler>(PageHandler.Mapper)
|
||||
{
|
||||
[nameof(ContentPage.Content)] = MapContent,
|
||||
[nameof(ContentPage.ToolbarItems)] = MapToolbarItems,
|
||||
};
|
||||
|
||||
public static new CommandMapper<ContentPage, ContentPageHandler> CommandMapper =
|
||||
@@ -131,24 +168,80 @@ public partial class ContentPageHandler : PageHandler
|
||||
return new SkiaContentPage();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaPage platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Sync toolbar items initially
|
||||
if (VirtualView is ContentPage contentPage && platformView is SkiaContentPage skiaContentPage)
|
||||
{
|
||||
SyncToolbarItems(skiaContentPage, contentPage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapContent(ContentPageHandler handler, ContentPage page)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||
|
||||
// Get the platform view for the content
|
||||
var content = page.Content;
|
||||
if (content != null)
|
||||
{
|
||||
// The content's handler should provide the platform view
|
||||
var contentHandler = content.Handler;
|
||||
if (contentHandler?.PlatformView is SkiaView skiaContent)
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
DiagnosticLog.Debug("ContentPageHandler", $"Creating handler for content: {content.GetType().Name}");
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
// The content's handler should provide the platform view
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
DiagnosticLog.Debug("ContentPageHandler", $"Setting content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.Content = skiaContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
DiagnosticLog.Warn("ContentPageHandler", $"Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for Picker on Linux using Skia rendering.
|
||||
/// Maps IPicker interface to SkiaPicker platform view.
|
||||
/// </summary>
|
||||
public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
{
|
||||
@@ -21,10 +22,13 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
[nameof(IPicker.TitleColor)] = MapTitleColor,
|
||||
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
|
||||
[nameof(IPicker.TextColor)] = MapTextColor,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
[nameof(Picker.ItemsSource)] = MapItemsSource,
|
||||
};
|
||||
|
||||
public static CommandMapper<IPicker, PickerHandler> CommandMapper =
|
||||
@@ -32,6 +36,8 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
{
|
||||
};
|
||||
|
||||
private INotifyCollectionChanged? _itemsCollection;
|
||||
|
||||
public PickerHandler() : base(Mapper, CommandMapper)
|
||||
{
|
||||
}
|
||||
@@ -51,21 +57,52 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
|
||||
|
||||
// Load items
|
||||
// Subscribe to items collection changes
|
||||
if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged items)
|
||||
{
|
||||
_itemsCollection = items;
|
||||
_itemsCollection.CollectionChanged += OnItemsCollectionChanged;
|
||||
}
|
||||
|
||||
// Load items and sync properties
|
||||
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)
|
||||
{
|
||||
platformView.SelectedIndexChanged -= OnSelectedIndexChanged;
|
||||
|
||||
if (_itemsCollection != null)
|
||||
{
|
||||
_itemsCollection.CollectionChanged -= OnItemsCollectionChanged;
|
||||
_itemsCollection = null;
|
||||
}
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnSelectedIndexChanged(object? sender, EventArgs e)
|
||||
private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ReloadItems();
|
||||
}
|
||||
|
||||
private void OnSelectedIndexChanged(object? sender, SelectedIndexChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
|
||||
VirtualView.SelectedIndex = PlatformView.SelectedIndex;
|
||||
if (VirtualView.SelectedIndex != e.NewIndex)
|
||||
{
|
||||
VirtualView.SelectedIndex = e.NewIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadItems()
|
||||
@@ -87,14 +124,18 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
if (handler.PlatformView is null) return;
|
||||
if (picker.TitleColor is not null)
|
||||
{
|
||||
handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor();
|
||||
handler.PlatformView.TitleColor = picker.TitleColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapSelectedIndex(PickerHandler handler, IPicker picker)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
|
||||
|
||||
if (handler.PlatformView.SelectedIndex != picker.SelectedIndex)
|
||||
{
|
||||
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapTextColor(PickerHandler handler, IPicker picker)
|
||||
@@ -102,23 +143,49 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
if (handler.PlatformView is null) return;
|
||||
if (picker.TextColor is not null)
|
||||
{
|
||||
handler.PlatformView.TextColor = picker.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = picker.TextColor;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// Character spacing could be implemented with custom text rendering
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.CharacterSpacing = picker.CharacterSpacing;
|
||||
}
|
||||
|
||||
public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker)
|
||||
{
|
||||
// Text alignment would require changes to SkiaPicker drawing
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.HorizontalTextAlignment = picker.HorizontalTextAlignment;
|
||||
}
|
||||
|
||||
public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker)
|
||||
{
|
||||
// Text alignment would require changes to SkiaPicker drawing
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.VerticalTextAlignment = picker.VerticalTextAlignment;
|
||||
}
|
||||
|
||||
public static void MapBackground(PickerHandler handler, IPicker picker)
|
||||
@@ -127,7 +194,19 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
|
||||
if (picker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
handler.ReloadItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +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;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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 Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -18,7 +20,12 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
{
|
||||
[nameof(IProgress.Progress)] = MapProgress,
|
||||
[nameof(IProgress.ProgressColor)] = MapProgressColor,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
[nameof(IView.Height)] = MapHeight,
|
||||
[nameof(IView.Width)] = MapWidth,
|
||||
["VerticalOptions"] = MapVerticalOptions,
|
||||
};
|
||||
|
||||
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -39,6 +46,48 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, 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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -50,7 +99,18 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
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)
|
||||
@@ -59,7 +119,49 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
|
||||
if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public partial class RadioButtonHandler : ViewHandler<IRadioButton, SkiaRadioBut
|
||||
|
||||
if (radioButton.TextColor is not null)
|
||||
{
|
||||
handler.PlatformView.TextColor = radioButton.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = radioButton.TextColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public partial class RadioButtonHandler : ViewHandler<IRadioButton, SkiaRadioBut
|
||||
|
||||
if (radioButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
Handlers/RefreshViewHandler.cs
Normal file
151
Handlers/RefreshViewHandler.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Handlers/ScrollViewHandler.cs
Normal file
111
Handlers/ScrollViewHandler.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ScrollView on Linux using SkiaScrollView.
|
||||
/// </summary>
|
||||
public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView>
|
||||
{
|
||||
public static IPropertyMapper<IScrollView, ScrollViewHandler> Mapper =
|
||||
new PropertyMapper<IScrollView, ScrollViewHandler>(ViewMapper)
|
||||
{
|
||||
[nameof(IScrollView.Content)] = MapContent,
|
||||
[nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility,
|
||||
[nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility,
|
||||
[nameof(IScrollView.Orientation)] = MapOrientation,
|
||||
};
|
||||
|
||||
public static CommandMapper<IScrollView, ScrollViewHandler> CommandMapper =
|
||||
new(ViewCommandMapper)
|
||||
{
|
||||
[nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo
|
||||
};
|
||||
|
||||
public ScrollViewHandler() : base(Mapper, CommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
public ScrollViewHandler(IPropertyMapper? mapper)
|
||||
: base(mapper ?? Mapper, CommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SkiaScrollView CreatePlatformView()
|
||||
{
|
||||
return new SkiaScrollView();
|
||||
}
|
||||
|
||||
public static void MapContent(ScrollViewHandler handler, IScrollView scrollView)
|
||||
{
|
||||
if (handler.PlatformView == null || handler.MauiContext == null)
|
||||
return;
|
||||
|
||||
var content = scrollView.PresentedContent;
|
||||
if (content != null)
|
||||
{
|
||||
DiagnosticLog.Debug("ScrollViewHandler", $"MapContent: {content.GetType().Name}");
|
||||
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
DiagnosticLog.Debug("ScrollViewHandler", $"Setting content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.Content = skiaContent;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.PlatformView.Content = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
|
||||
{
|
||||
handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch
|
||||
{
|
||||
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
|
||||
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
|
||||
_ => ScrollBarVisibility.Default
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
|
||||
{
|
||||
handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch
|
||||
{
|
||||
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
|
||||
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
|
||||
_ => ScrollBarVisibility.Default
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView)
|
||||
{
|
||||
handler.PlatformView.Orientation = scrollView.Orientation switch
|
||||
{
|
||||
Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal,
|
||||
Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both,
|
||||
Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither,
|
||||
_ => ScrollOrientation.Vertical
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args)
|
||||
{
|
||||
if (args is ScrollToRequest request)
|
||||
{
|
||||
// Instant means no animation, so we pass !Instant for animated parameter
|
||||
handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,11 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
[nameof(ITextInput.Text)] = MapText,
|
||||
[nameof(ITextStyle.TextColor)] = MapTextColor,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(IPlaceholder.Placeholder)] = MapPlaceholder,
|
||||
[nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor,
|
||||
[nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor,
|
||||
[nameof(ISearchBar.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
};
|
||||
|
||||
@@ -84,7 +86,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (searchBar.TextColor is not null)
|
||||
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = searchBar.TextColor;
|
||||
}
|
||||
|
||||
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
|
||||
@@ -93,10 +95,28 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
|
||||
var font = searchBar.Font;
|
||||
if (font.Size > 0)
|
||||
handler.PlatformView.FontSize = (float)font.Size;
|
||||
handler.PlatformView.FontSize = font.Size;
|
||||
|
||||
if (!string.IsNullOrEmpty(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)
|
||||
@@ -110,7 +130,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (searchBar.PlaceholderColor is not null)
|
||||
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
|
||||
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor;
|
||||
}
|
||||
|
||||
public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)
|
||||
@@ -119,7 +139,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
|
||||
// CancelButtonColor maps to ClearButtonColor
|
||||
if (searchBar.CancelButtonColor is not null)
|
||||
handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor.ToSKColor();
|
||||
handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +149,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
||||
|
||||
if (searchBar.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// 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 Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -10,14 +13,29 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
/// <summary>
|
||||
/// Handler for Shell on Linux using Skia rendering.
|
||||
/// </summary>
|
||||
public partial class ShellHandler : ViewHandler<IView, SkiaShell>
|
||||
public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
|
||||
{
|
||||
public static IPropertyMapper<IView, ShellHandler> Mapper = new PropertyMapper<IView, ShellHandler>(ViewHandler.ViewMapper)
|
||||
private bool _isUpdatingFlyoutPresented;
|
||||
|
||||
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<IView, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
public static CommandMapper<Shell, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
{
|
||||
["GoToAsync"] = MapGoToAsync,
|
||||
};
|
||||
|
||||
public ShellHandler() : base(Mapper, CommandMapper)
|
||||
@@ -31,30 +49,372 @@ public partial class ShellHandler : ViewHandler<IView, SkiaShell>
|
||||
|
||||
protected override SkiaShell CreatePlatformView()
|
||||
{
|
||||
DiagnosticLog.Debug("ShellHandler", "CreatePlatformView - creating SkiaShell");
|
||||
return new SkiaShell();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaShell platformView)
|
||||
{
|
||||
DiagnosticLog.Debug("ShellHandler", "ConnectHandler - connecting to SkiaShell");
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
||||
platformView.Navigated += OnNavigated;
|
||||
|
||||
// Store reference to MAUI Shell for callbacks
|
||||
platformView.MauiShell = VirtualView;
|
||||
|
||||
// Set up content renderer
|
||||
platformView.ContentRenderer = RenderShellContent;
|
||||
platformView.ColorRefresher = RefreshShellColors;
|
||||
|
||||
// Subscribe to Shell navigation events
|
||||
if (VirtualView != null)
|
||||
{
|
||||
VirtualView.Navigating += OnShellNavigating;
|
||||
VirtualView.Navigated += OnShellNavigated;
|
||||
|
||||
// Initial sync of shell items
|
||||
SyncShellItems();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaShell platformView)
|
||||
{
|
||||
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
|
||||
platformView.Navigated -= OnNavigated;
|
||||
platformView.MauiShell = null;
|
||||
platformView.ContentRenderer = null;
|
||||
platformView.ColorRefresher = null;
|
||||
|
||||
if (VirtualView != null)
|
||||
{
|
||||
VirtualView.Navigating -= OnShellNavigating;
|
||||
VirtualView.Navigated -= OnShellNavigated;
|
||||
}
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Sync flyout state to virtual view
|
||||
if (VirtualView is null || PlatformView is null || _isUpdatingFlyoutPresented) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isUpdatingFlyoutPresented = true;
|
||||
VirtualView.FlyoutIsPresented = PlatformView.FlyoutIsPresented;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingFlyoutPresented = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNavigated(object? sender, ShellNavigationEventArgs e)
|
||||
private void OnNavigated(object? sender, Platform.ShellNavigationEventArgs e)
|
||||
{
|
||||
// Handle navigation events
|
||||
// Handle platform navigation events
|
||||
}
|
||||
|
||||
private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("ShellHandler", $"Shell Navigating to: {e.Target?.Location}");
|
||||
|
||||
// Route to platform view
|
||||
if (PlatformView != null && e.Target?.Location != null)
|
||||
{
|
||||
var route = e.Target.Location.ToString().TrimStart('/');
|
||||
DiagnosticLog.Debug("ShellHandler", $"Routing to: {route}");
|
||||
PlatformView.GoToAsync(route);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +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;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
[nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor,
|
||||
[nameof(ISlider.ThumbColor)] = MapThumbColor,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -48,6 +49,15 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
platformView.ValueChanged += OnValueChanged;
|
||||
platformView.DragStarted += OnDragStarted;
|
||||
platformView.DragCompleted += OnDragCompleted;
|
||||
|
||||
// Sync properties that may have been set before handler connection
|
||||
if (VirtualView != null)
|
||||
{
|
||||
MapMinimum(this, VirtualView);
|
||||
MapMaximum(this, VirtualView);
|
||||
MapValue(this, VirtualView);
|
||||
MapIsEnabled(this, VirtualView);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaSlider platformView)
|
||||
@@ -58,7 +68,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
|
||||
private void OnValueChanged(object? sender, ValueChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
|
||||
@@ -102,18 +112,16 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// MinimumTrackColor maps to ActiveTrackColor (the filled portion)
|
||||
if (slider.MinimumTrackColor is not null)
|
||||
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
|
||||
handler.PlatformView.MinimumTrackColor = slider.MinimumTrackColor;
|
||||
}
|
||||
|
||||
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// MaximumTrackColor maps to TrackColor (the unfilled portion)
|
||||
if (slider.MaximumTrackColor is not null)
|
||||
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
|
||||
handler.PlatformView.MaximumTrackColor = slider.MaximumTrackColor;
|
||||
}
|
||||
|
||||
public static void MapThumbColor(SliderHandler handler, ISlider slider)
|
||||
@@ -121,7 +129,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (slider.ThumbColor is not null)
|
||||
handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor();
|
||||
handler.PlatformView.ThumbColor = slider.ThumbColor;
|
||||
}
|
||||
|
||||
public static void MapBackground(SliderHandler handler, ISlider slider)
|
||||
@@ -130,7 +138,14 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
||||
|
||||
if (slider.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = slider.IsEnabled;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for Stepper on Linux using Skia rendering.
|
||||
/// Maps IStepper interface to SkiaStepper platform view.
|
||||
/// </summary>
|
||||
public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||
{
|
||||
@@ -19,7 +20,9 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||
[nameof(IStepper.Value)] = MapValue,
|
||||
[nameof(IStepper.Minimum)] = MapMinimum,
|
||||
[nameof(IStepper.Maximum)] = MapMaximum,
|
||||
["Increment"] = MapIncrement,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<IStepper, StepperHandler> CommandMapper =
|
||||
@@ -45,6 +48,26 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
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)
|
||||
@@ -53,16 +76,22 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnValueChanged(object? sender, EventArgs e)
|
||||
private void OnValueChanged(object? sender, ValueChangedEventArgs e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.Value = stepper.Value;
|
||||
|
||||
if (Math.Abs(handler.PlatformView.Value - stepper.Value) > 0.0001)
|
||||
handler.PlatformView.Value = stepper.Value;
|
||||
}
|
||||
|
||||
public static void MapMinimum(StepperHandler handler, IStepper stepper)
|
||||
@@ -83,7 +112,24 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||
|
||||
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
226
Handlers/SwipeViewHandler.cs
Normal file
226
Handlers/SwipeViewHandler.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +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;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
[nameof(ISwitch.TrackColor)] = MapTrackColor,
|
||||
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -69,13 +70,12 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// TrackColor sets both On and Off track colors
|
||||
// TrackColor sets the On track color (MAUI's OnColor)
|
||||
if (@switch.TrackColor is not null)
|
||||
{
|
||||
var color = @switch.TrackColor.ToSKColor();
|
||||
handler.PlatformView.OnTrackColor = color;
|
||||
// Off track could be a lighter version
|
||||
handler.PlatformView.OffTrackColor = color.WithAlpha(128);
|
||||
handler.PlatformView.OnTrackColor = @switch.TrackColor;
|
||||
// Off track is a lighter/desaturated version
|
||||
handler.PlatformView.OffTrackColor = @switch.TrackColor.WithAlpha(0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (@switch.ThumbColor is not null)
|
||||
handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor();
|
||||
handler.PlatformView.ThumbColor = @switch.ThumbColor;
|
||||
}
|
||||
|
||||
public static void MapBackground(SwitchHandler handler, ISwitch @switch)
|
||||
@@ -93,7 +93,14 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
|
||||
if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
// Background color for the switch container (not the track)
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = @switch.IsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// 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;
|
||||
@@ -13,8 +15,14 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
/// </summary>
|
||||
public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage>
|
||||
{
|
||||
private bool _isUpdatingSelection;
|
||||
|
||||
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)
|
||||
@@ -39,6 +47,9 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
|
||||
|
||||
// Sync initial tabs
|
||||
SyncTabs();
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaTabbedPage platformView)
|
||||
@@ -50,6 +61,104 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
|
||||
|
||||
private void OnSelectedIndexChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Notify the virtual view of selection change
|
||||
if (VirtualView is null || PlatformView is null || _isUpdatingSelection) return;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
||||
[nameof(ITimePicker.Format)] = MapFormat,
|
||||
[nameof(ITimePicker.TextColor)] = MapTextColor,
|
||||
[nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
};
|
||||
|
||||
@@ -47,6 +48,16 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
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)
|
||||
@@ -55,11 +66,11 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnTimeSelected(object? sender, EventArgs e)
|
||||
private void OnTimeSelected(object? sender, TimeChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
|
||||
VirtualView.Time = PlatformView.Time;
|
||||
VirtualView.Time = e.NewTime;
|
||||
}
|
||||
|
||||
public static void MapTime(TimePickerHandler handler, ITimePicker timePicker)
|
||||
@@ -79,13 +90,32 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
||||
if (handler.PlatformView is null) return;
|
||||
if (timePicker.TextColor is not null)
|
||||
{
|
||||
handler.PlatformView.TextColor = timePicker.TextColor.ToSKColor();
|
||||
handler.PlatformView.TextColor = timePicker.TextColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapCharacterSpacing(TimePickerHandler handler, ITimePicker timePicker)
|
||||
{
|
||||
// Character spacing would require custom text rendering
|
||||
if (handler.PlatformView is null) return;
|
||||
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)
|
||||
@@ -94,7 +124,7 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
||||
|
||||
if (timePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
208
Handlers/WebViewHandler.Linux.cs
Normal file
208
Handlers/WebViewHandler.Linux.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
196
Handlers/WebViewHandler.cs
Normal file
196
Handlers/WebViewHandler.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for WebView control on Linux using WebKitGTK.
|
||||
/// </summary>
|
||||
public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
|
||||
{
|
||||
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
|
||||
{
|
||||
[nameof(IWebView.Source)] = MapSource,
|
||||
[nameof(IWebView.UserAgent)] = MapUserAgent,
|
||||
};
|
||||
|
||||
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 = null, CommandMapper? commandMapper = null)
|
||||
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SkiaWebView CreatePlatformView()
|
||||
{
|
||||
return new SkiaWebView();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaWebView platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
platformView.Navigating += OnNavigating;
|
||||
platformView.Navigated += OnNavigated;
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaWebView platformView)
|
||||
{
|
||||
platformView.Navigating -= OnNavigating;
|
||||
platformView.Navigated -= OnNavigated;
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnNavigating(object? sender, Microsoft.Maui.Platform.WebNavigatingEventArgs e)
|
||||
{
|
||||
IWebView virtualView = VirtualView;
|
||||
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)
|
||||
{
|
||||
IWebView virtualView = VirtualView;
|
||||
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)
|
||||
{
|
||||
DiagnosticLog.Debug("WebViewHandler", "MapSource called");
|
||||
if (handler.PlatformView == null)
|
||||
{
|
||||
DiagnosticLog.Warn("WebViewHandler", "PlatformView is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = webView.Source;
|
||||
DiagnosticLog.Debug("WebViewHandler", $"Source type: {source?.GetType().Name ?? "null"}");
|
||||
|
||||
if (source is UrlWebViewSource urlSource)
|
||||
{
|
||||
DiagnosticLog.Debug("WebViewHandler", $"Loading URL: {urlSource.Url}");
|
||||
handler.PlatformView.Source = urlSource.Url ?? "";
|
||||
}
|
||||
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 ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
DiagnosticLog.Debug("WebViewHandler", "Unknown source type or null");
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
|
||||
{
|
||||
handler.PlatformView?.GoBack();
|
||||
}
|
||||
|
||||
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
|
||||
{
|
||||
handler.PlatformView?.GoForward();
|
||||
}
|
||||
|
||||
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
|
||||
{
|
||||
handler.PlatformView?.Reload();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -81,13 +82,20 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
||||
|
||||
public static void MapContent(WindowHandler handler, IWindow window)
|
||||
{
|
||||
DiagnosticLog.Debug("WindowHandler", $"MapContent - PlatformView={handler.PlatformView != null}");
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
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)
|
||||
{
|
||||
DiagnosticLog.Debug("WindowHandler", $"MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
|
||||
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)
|
||||
@@ -141,6 +149,7 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
||||
|
||||
/// <summary>
|
||||
/// Skia window wrapper for Linux display servers.
|
||||
/// Handles rendering of content and popup overlays automatically.
|
||||
/// </summary>
|
||||
public class SkiaWindow
|
||||
{
|
||||
@@ -164,6 +173,28 @@ public class SkiaWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the window content and popup overlays to the canvas.
|
||||
/// This should be called by the platform rendering loop.
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
// Clear background
|
||||
canvas.Clear(SKColors.White);
|
||||
|
||||
// Draw main content
|
||||
if (_content != null)
|
||||
{
|
||||
_content.Measure(new Size(_width, _height));
|
||||
_content.Arrange(new Rect(0, 0, _width, _height));
|
||||
_content.Draw(canvas);
|
||||
}
|
||||
|
||||
// Draw popup overlays on top (dropdowns, date pickers, etc.)
|
||||
// This ensures popups always render above all other content
|
||||
SkiaView.DrawPopupOverlays(canvas);
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
|
||||
52
Hosting/GtkMauiContext.cs
Normal file
52
Hosting/GtkMauiContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Hosting/HandlerMappingExtensions.cs
Normal file
17
Hosting/HandlerMappingExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
56
Hosting/LinuxAnimationManager.cs
Normal file
56
Hosting/LinuxAnimationManager.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,47 @@
|
||||
// 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 Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.ApplicationModel.Communication;
|
||||
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Devices;
|
||||
using Microsoft.Maui.Dispatching;
|
||||
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.Storage;
|
||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||
using Microsoft.Maui.Controls;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring MAUI applications for Linux.
|
||||
/// </summary>
|
||||
public static class LinuxMauiAppBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the MAUI application to run on Linux.
|
||||
/// </summary>
|
||||
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
|
||||
{
|
||||
return builder.UseLinux(configure: null);
|
||||
return builder.UseLinux(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the MAUI application to run on Linux with options.
|
||||
/// </summary>
|
||||
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure)
|
||||
{
|
||||
var options = new LinuxApplicationOptions();
|
||||
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
|
||||
builder.Services.TryAddSingleton<ILauncher, LauncherService>();
|
||||
builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
|
||||
@@ -47,51 +54,99 @@ public static class LinuxMauiAppBuilderExtensions
|
||||
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
|
||||
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
|
||||
RegisterTypeConverters();
|
||||
|
||||
// Register Linux-specific handlers
|
||||
builder.ConfigureMauiHandlers(handlers =>
|
||||
{
|
||||
// Phase 1 - MVP controls
|
||||
handlers.AddHandler<IButton, ButtonHandler>();
|
||||
handlers.AddHandler<ILabel, LabelHandler>();
|
||||
handlers.AddHandler<IEntry, EntryHandler>();
|
||||
handlers.AddHandler<ICheckBox, CheckBoxHandler>();
|
||||
handlers.AddHandler<ILayout, LayoutHandler>();
|
||||
handlers.AddHandler<IStackLayout, StackLayoutHandler>();
|
||||
handlers.AddHandler<IGridLayout, GridHandler>();
|
||||
// Application handler
|
||||
handlers.AddHandler<IApplication, ApplicationHandler>();
|
||||
|
||||
// Phase 2 - Input controls
|
||||
handlers.AddHandler<ISlider, SliderHandler>();
|
||||
handlers.AddHandler<ISwitch, SwitchHandler>();
|
||||
handlers.AddHandler<IProgress, ProgressBarHandler>();
|
||||
handlers.AddHandler<IActivityIndicator, ActivityIndicatorHandler>();
|
||||
handlers.AddHandler<ISearchBar, SearchBarHandler>();
|
||||
// Core controls
|
||||
handlers.AddHandler<BoxView, BoxViewHandler>();
|
||||
handlers.AddHandler<Button, TextButtonHandler>();
|
||||
handlers.AddHandler<Label, LabelHandler>();
|
||||
handlers.AddHandler<Entry, EntryHandler>();
|
||||
handlers.AddHandler<Editor, EditorHandler>();
|
||||
handlers.AddHandler<CheckBox, CheckBoxHandler>();
|
||||
handlers.AddHandler<Switch, SwitchHandler>();
|
||||
handlers.AddHandler<Slider, SliderHandler>();
|
||||
handlers.AddHandler<Stepper, StepperHandler>();
|
||||
handlers.AddHandler<RadioButton, RadioButtonHandler>();
|
||||
|
||||
// Phase 2 - Image & Graphics
|
||||
handlers.AddHandler<IImage, ImageHandler>();
|
||||
handlers.AddHandler<IImageButton, ImageButtonHandler>();
|
||||
handlers.AddHandler<IGraphicsView, GraphicsViewHandler>();
|
||||
// Layout controls
|
||||
handlers.AddHandler<Grid, GridHandler>();
|
||||
handlers.AddHandler<StackLayout, StackLayoutHandler>();
|
||||
handlers.AddHandler<VerticalStackLayout, StackLayoutHandler>();
|
||||
handlers.AddHandler<HorizontalStackLayout, StackLayoutHandler>();
|
||||
handlers.AddHandler<AbsoluteLayout, LayoutHandler>();
|
||||
handlers.AddHandler<FlexLayout, FlexLayoutHandler>();
|
||||
handlers.AddHandler<ScrollView, ScrollViewHandler>();
|
||||
handlers.AddHandler<Frame, FrameHandler>();
|
||||
handlers.AddHandler<Border, BorderHandler>();
|
||||
handlers.AddHandler<ContentView, BorderHandler>();
|
||||
handlers.AddHandler<RefreshView, RefreshViewHandler>();
|
||||
|
||||
// Phase 3 - Collection Views
|
||||
// Picker controls
|
||||
handlers.AddHandler<Picker, PickerHandler>();
|
||||
handlers.AddHandler<DatePicker, DatePickerHandler>();
|
||||
handlers.AddHandler<TimePicker, TimePickerHandler>();
|
||||
handlers.AddHandler<SearchBar, SearchBarHandler>();
|
||||
|
||||
// Progress & Activity
|
||||
handlers.AddHandler<ProgressBar, ProgressBarHandler>();
|
||||
handlers.AddHandler<ActivityIndicator, ActivityIndicatorHandler>();
|
||||
|
||||
// Image & Graphics
|
||||
handlers.AddHandler<Image, ImageHandler>();
|
||||
handlers.AddHandler<ImageButton, ImageButtonHandler>();
|
||||
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
|
||||
|
||||
// Web - use GtkWebViewHandler
|
||||
handlers.AddHandler<WebView, GtkWebViewHandler>();
|
||||
|
||||
// Collection Views
|
||||
handlers.AddHandler<CollectionView, CollectionViewHandler>();
|
||||
handlers.AddHandler<ListView, CollectionViewHandler>();
|
||||
handlers.AddHandler<CarouselView, CarouselViewHandler>();
|
||||
handlers.AddHandler<IndicatorView, IndicatorViewHandler>();
|
||||
handlers.AddHandler<SwipeView, SwipeViewHandler>();
|
||||
|
||||
// Phase 4 - Pages & Navigation
|
||||
// Pages & Navigation
|
||||
handlers.AddHandler<Page, PageHandler>();
|
||||
handlers.AddHandler<ContentPage, ContentPageHandler>();
|
||||
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
|
||||
handlers.AddHandler<Shell, ShellHandler>();
|
||||
handlers.AddHandler<FlyoutPage, FlyoutPageHandler>();
|
||||
handlers.AddHandler<TabbedPage, TabbedPageHandler>();
|
||||
|
||||
// Phase 5 - Advanced Controls
|
||||
handlers.AddHandler<IPicker, PickerHandler>();
|
||||
handlers.AddHandler<IDatePicker, DatePickerHandler>();
|
||||
handlers.AddHandler<ITimePicker, TimePickerHandler>();
|
||||
handlers.AddHandler<IEditor, EditorHandler>();
|
||||
|
||||
// Phase 7 - Additional Controls
|
||||
handlers.AddHandler<IStepper, StepperHandler>();
|
||||
handlers.AddHandler<IRadioButton, RadioButtonHandler>();
|
||||
handlers.AddHandler<IBorderView, BorderHandler>();
|
||||
|
||||
// Window handler
|
||||
handlers.AddHandler<IWindow, WindowHandler>();
|
||||
// Application & Window
|
||||
handlers.AddHandler<Application, ApplicationHandler>();
|
||||
handlers.AddHandler<Microsoft.Maui.Controls.Window, WindowHandler>();
|
||||
});
|
||||
|
||||
// Store options for later use
|
||||
@@ -99,22 +154,12 @@ public static class LinuxMauiAppBuilderExtensions
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
private static void RegisterTypeConverters()
|
||||
{
|
||||
handlers.AddHandler(typeof(TView), typeof(THandler));
|
||||
return handlers;
|
||||
TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter)));
|
||||
TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter)));
|
||||
TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter)));
|
||||
TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter)));
|
||||
}
|
||||
}
|
||||
|
||||
51
Hosting/LinuxMauiContext.cs
Normal file
51
Hosting/LinuxMauiContext.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 LinuxMauiContext : IMauiContext
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IMauiHandlersFactory _handlers;
|
||||
private readonly LinuxApplication _linuxApp;
|
||||
private IAnimationManager? _animationManager;
|
||||
private IDispatcher? _dispatcher;
|
||||
|
||||
public IServiceProvider Services => _services;
|
||||
|
||||
public IMauiHandlersFactory Handlers => _handlers;
|
||||
|
||||
public LinuxApplication LinuxApp => _linuxApp;
|
||||
|
||||
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 LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp)
|
||||
{
|
||||
_services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
_linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp));
|
||||
_handlers = services.GetRequiredService<IMauiHandlersFactory>();
|
||||
}
|
||||
}
|
||||
@@ -2,41 +2,159 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Maui.Hosting;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Hosting;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for running MAUI applications on Linux.
|
||||
/// </summary>
|
||||
public static class LinuxProgramHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the MAUI application on Linux.
|
||||
/// </summary>
|
||||
/// <typeparam name="TApp">The application type.</typeparam>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
public static void Run<TApp>(string[] args) where TApp : class, IApplication, new()
|
||||
{
|
||||
Run<TApp>(args, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the MAUI application on Linux with additional configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TApp">The application type.</typeparam>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
/// <param name="configure">Optional builder configuration action.</param>
|
||||
public static void Run<TApp>(string[] args, Action<MauiAppBuilder>? configure) where TApp : class, IApplication, new()
|
||||
{
|
||||
// Build the MAUI application
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder.UseLinux();
|
||||
configure?.Invoke(builder);
|
||||
builder.UseMauiApp<TApp>();
|
||||
var mauiApp = builder.Build();
|
||||
|
||||
// Get application options
|
||||
var options = mauiApp.Services.GetService<LinuxApplicationOptions>()
|
||||
?? new LinuxApplicationOptions();
|
||||
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
|
||||
using var linuxApp = new LinuxApplication();
|
||||
linuxApp.Initialize(options);
|
||||
|
||||
// Create comprehensive demo UI with ALL controls
|
||||
var rootView = CreateComprehensiveDemo();
|
||||
linuxApp.RootView = rootView;
|
||||
// Create MAUI context
|
||||
var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp);
|
||||
|
||||
// Get the MAUI application instance
|
||||
var application = mauiApp.Services.GetService<IApplication>();
|
||||
|
||||
// Ensure Application.Current is set - required for Shell.Current to work
|
||||
if (application is Application app && Application.Current == null)
|
||||
{
|
||||
// Use reflection to set Current since it has a protected setter
|
||||
var currentProperty = typeof(Application).GetProperty("Current");
|
||||
currentProperty?.SetValue(null, app);
|
||||
}
|
||||
|
||||
// Try to render the application's main page
|
||||
SkiaView? rootView = null;
|
||||
|
||||
if (application != null)
|
||||
{
|
||||
rootView = RenderApplication(application, mauiContext, options);
|
||||
}
|
||||
|
||||
// Fallback to demo if no application view is available
|
||||
if (rootView == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxProgramHost", "No application page found. Showing demo UI.");
|
||||
rootView = CreateDemoView();
|
||||
}
|
||||
|
||||
linuxApp.RootView = rootView;
|
||||
linuxApp.Run();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the MAUI application and returns the root SkiaView.
|
||||
/// </summary>
|
||||
private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For Applications, we need to create a window
|
||||
if (application is Application app)
|
||||
{
|
||||
Page? mainPage = app.MainPage;
|
||||
|
||||
// If no MainPage set, check for windows
|
||||
if (mainPage == null && application.Windows.Count > 0)
|
||||
{
|
||||
var existingWindow = application.Windows[0];
|
||||
if (existingWindow.Content is Page page)
|
||||
{
|
||||
mainPage = page;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainPage != null)
|
||||
{
|
||||
// Create a MAUI Window and add it to the application
|
||||
// This ensures Shell.Current works properly (it reads from Application.Current.Windows[0].Page)
|
||||
if (app.Windows.Count == 0)
|
||||
{
|
||||
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
|
||||
|
||||
// Try OpenWindow first
|
||||
app.OpenWindow(mauiWindow);
|
||||
|
||||
// If that didn't work, use reflection to add directly to _windows
|
||||
if (app.Windows.Count == 0)
|
||||
{
|
||||
var windowsField = typeof(Application).GetField("_windows",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
if (windowsField?.GetValue(app) is System.Collections.IList windowsList)
|
||||
{
|
||||
windowsList.Add(mauiWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RenderPage(mainPage, mauiContext);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("LinuxProgramHost", $"Error rendering application: {ex.Message}");
|
||||
DiagnosticLog.Error("LinuxProgramHost", ex.StackTrace ?? "");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a MAUI Page to a SkiaView.
|
||||
/// </summary>
|
||||
private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext)
|
||||
{
|
||||
var renderer = new LinuxViewRenderer(mauiContext);
|
||||
return renderer.RenderPage(page);
|
||||
}
|
||||
|
||||
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
|
||||
{
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
@@ -54,70 +172,77 @@ public static class LinuxProgramHost
|
||||
options.Height = h;
|
||||
i++;
|
||||
break;
|
||||
case "--demo":
|
||||
// Force demo mode
|
||||
options.ForceDemo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SkiaView CreateComprehensiveDemo()
|
||||
/// <summary>
|
||||
/// Creates a demo view showcasing all controls.
|
||||
/// </summary>
|
||||
public static SkiaView CreateDemoView()
|
||||
{
|
||||
// Create scrollable container
|
||||
var scroll = new SkiaScrollView();
|
||||
|
||||
|
||||
var root = new SkiaStackLayout
|
||||
{
|
||||
Orientation = StackOrientation.Vertical,
|
||||
Spacing = 15,
|
||||
BackgroundColor = new SKColor(0xF5, 0xF5, 0xF5)
|
||||
BackgroundColor = Color.FromRgb(0xF5, 0xF5, 0xF5)
|
||||
};
|
||||
root.Padding = new SKRect(20, 20, 20, 20);
|
||||
root.Padding = new Thickness(20, 20, 20, 20);
|
||||
|
||||
// ========== TITLE ==========
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "MAUI Linux Control Demo",
|
||||
FontSize = 28,
|
||||
TextColor = new SKColor(0x1A, 0x23, 0x7E),
|
||||
IsBold = true
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "OpenMaui Linux Control Demo",
|
||||
FontSize = 28,
|
||||
TextColor = Color.FromRgb(0x1A, 0x23, 0x7E),
|
||||
FontAttributes = FontAttributes.Bold
|
||||
});
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "All controls rendered using SkiaSharp on X11",
|
||||
FontSize = 14,
|
||||
TextColor = SKColors.Gray
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "All controls rendered using SkiaSharp on X11",
|
||||
FontSize = 14,
|
||||
TextColor = Colors.Gray
|
||||
});
|
||||
|
||||
// ========== LABELS SECTION ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("Labels"));
|
||||
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black });
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true });
|
||||
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 = new SKColor(0xE9, 0x1E, 0x63) });
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = Colors.Black });
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = Colors.Black, FontAttributes = FontAttributes.Bold });
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = Colors.Gray, FontAttributes = FontAttributes.Italic });
|
||||
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = Color.FromRgb(0xE9, 0x1E, 0x63) });
|
||||
root.AddChild(labelSection);
|
||||
|
||||
// ========== BUTTONS SECTION ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("Buttons"));
|
||||
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||
|
||||
|
||||
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
|
||||
btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3);
|
||||
btnPrimary.TextColor = SKColors.White;
|
||||
btnPrimary.BackgroundColor = Color.FromRgb(0x21, 0x96, 0xF3);
|
||||
btnPrimary.TextColor = Colors.White;
|
||||
var clickCount = 0;
|
||||
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
|
||||
buttonSection.AddChild(btnPrimary);
|
||||
|
||||
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
|
||||
btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50);
|
||||
btnSuccess.TextColor = SKColors.White;
|
||||
btnSuccess.BackgroundColor = Color.FromRgb(0x4C, 0xAF, 0x50);
|
||||
btnSuccess.TextColor = Colors.White;
|
||||
buttonSection.AddChild(btnSuccess);
|
||||
|
||||
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
|
||||
btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36);
|
||||
btnDanger.TextColor = SKColors.White;
|
||||
btnDanger.BackgroundColor = Color.FromRgb(0xF4, 0x43, 0x36);
|
||||
btnDanger.TextColor = Colors.White;
|
||||
buttonSection.AddChild(btnDanger);
|
||||
|
||||
|
||||
root.AddChild(buttonSection);
|
||||
|
||||
// ========== ENTRY SECTION ==========
|
||||
@@ -130,7 +255,7 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("SearchBar"));
|
||||
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
|
||||
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray };
|
||||
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = Colors.Gray };
|
||||
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
|
||||
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
|
||||
root.AddChild(searchBar);
|
||||
@@ -139,11 +264,11 @@ public static class LinuxProgramHost
|
||||
// ========== EDITOR SECTION ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("Editor (Multi-line)"));
|
||||
var editor = new SkiaEditor
|
||||
{
|
||||
Placeholder = "Enter multiple lines of text...",
|
||||
var editor = new SkiaEditor
|
||||
{
|
||||
Placeholder = "Enter multiple lines of text...",
|
||||
FontSize = 14,
|
||||
BackgroundColor = SKColors.White
|
||||
BackgroundColor = Colors.White
|
||||
};
|
||||
root.AddChild(editor);
|
||||
|
||||
@@ -205,7 +330,7 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSectionHeader("ProgressBar"));
|
||||
var progress = new SkiaProgressBar { Progress = 0.7f };
|
||||
root.AddChild(progress);
|
||||
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray });
|
||||
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = Colors.Gray });
|
||||
|
||||
// ========== ACTIVITYINDICATOR SECTION ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
@@ -213,7 +338,7 @@ public static class LinuxProgramHost
|
||||
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||
var activity = new SkiaActivityIndicator { IsRunning = true };
|
||||
activitySection.AddChild(activity);
|
||||
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray });
|
||||
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = Colors.Gray });
|
||||
root.AddChild(activitySection);
|
||||
|
||||
// ========== PICKER SECTION ==========
|
||||
@@ -221,7 +346,7 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
|
||||
var picker = new SkiaPicker { Title = "Select an item" };
|
||||
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
|
||||
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
|
||||
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
|
||||
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
|
||||
root.AddChild(picker);
|
||||
root.AddChild(pickerLabel);
|
||||
@@ -230,7 +355,7 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("DatePicker"));
|
||||
var datePicker = new SkiaDatePicker { Date = DateTime.Today };
|
||||
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = SKColors.Gray };
|
||||
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = Colors.Gray };
|
||||
datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}";
|
||||
root.AddChild(datePicker);
|
||||
root.AddChild(dateLabel);
|
||||
@@ -239,7 +364,7 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("TimePicker"));
|
||||
var timePicker = new SkiaTimePicker();
|
||||
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = SKColors.Gray };
|
||||
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = Colors.Gray };
|
||||
timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}";
|
||||
root.AddChild(timePicker);
|
||||
root.AddChild(timeLabel);
|
||||
@@ -251,18 +376,18 @@ public static class LinuxProgramHost
|
||||
{
|
||||
CornerRadius = 8,
|
||||
StrokeThickness = 2,
|
||||
Stroke = new SKColor(0x21, 0x96, 0xF3),
|
||||
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD)
|
||||
Stroke = Color.FromRgb(0x21, 0x96, 0xF3),
|
||||
BackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD)
|
||||
};
|
||||
border.SetPadding(15);
|
||||
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = new SKColor(0x1A, 0x23, 0x7E) });
|
||||
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = Color.FromRgb(0x1A, 0x23, 0x7E) });
|
||||
root.AddChild(border);
|
||||
|
||||
// ========== FRAME SECTION ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("Frame (with shadow)"));
|
||||
var frame = new SkiaFrame();
|
||||
frame.BackgroundColor = SKColors.White;
|
||||
frame.BackgroundColor = Colors.White;
|
||||
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
|
||||
root.AddChild(frame);
|
||||
|
||||
@@ -276,8 +401,8 @@ public static class LinuxProgramHost
|
||||
Footer = "End of list"
|
||||
};
|
||||
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
|
||||
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
|
||||
collectionView.SelectionChanged += (s, e) =>
|
||||
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
|
||||
collectionView.SelectionChanged += (s, e) =>
|
||||
{
|
||||
var selected = e.CurrentSelection.FirstOrDefault();
|
||||
collectionLabel.Text = $"Selected: {selected}";
|
||||
@@ -289,23 +414,20 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("ImageButton"));
|
||||
var imageButtonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||
|
||||
|
||||
// Create ImageButton with a generated icon (since we don't have image files)
|
||||
var imgBtn = new SkiaImageButton
|
||||
{
|
||||
CornerRadius = 8,
|
||||
StrokeColor = new SKColor(0x21, 0x96, 0xF3),
|
||||
StrokeColor = Color.FromRgb(0x21, 0x96, 0xF3),
|
||||
StrokeThickness = 1,
|
||||
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD),
|
||||
PaddingLeft = 10,
|
||||
PaddingRight = 10,
|
||||
PaddingTop = 10,
|
||||
PaddingBottom = 10
|
||||
ImageBackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD),
|
||||
Padding = new Thickness(10)
|
||||
};
|
||||
// Generate a simple star icon bitmap
|
||||
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
|
||||
imgBtn.Bitmap = iconBitmap;
|
||||
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray };
|
||||
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = Colors.Gray };
|
||||
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
|
||||
imageButtonSection.AddChild(imgBtn);
|
||||
imageButtonSection.AddChild(imgBtnLabel);
|
||||
@@ -315,29 +437,29 @@ public static class LinuxProgramHost
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(CreateSectionHeader("Image"));
|
||||
var imageSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||
|
||||
|
||||
// Create Image with a generated sample image
|
||||
var img = new SkiaImage();
|
||||
var sampleBitmap = CreateSampleImage(80, 60);
|
||||
img.Bitmap = sampleBitmap;
|
||||
imageSection.AddChild(img);
|
||||
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray });
|
||||
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = Colors.Gray });
|
||||
root.AddChild(imageSection);
|
||||
|
||||
// ========== FOOTER ==========
|
||||
root.AddChild(CreateSeparator());
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "All 25+ controls are interactive - try them all!",
|
||||
FontSize = 16,
|
||||
TextColor = new SKColor(0x4C, 0xAF, 0x50),
|
||||
IsBold = true
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "All 25+ controls are interactive - try them all!",
|
||||
FontSize = 16,
|
||||
TextColor = Color.FromRgb(0x4C, 0xAF, 0x50),
|
||||
FontAttributes = FontAttributes.Bold
|
||||
});
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "Scroll down to see more controls",
|
||||
FontSize = 12,
|
||||
TextColor = SKColors.Gray
|
||||
root.AddChild(new SkiaLabel
|
||||
{
|
||||
Text = "Scroll down to see more controls",
|
||||
FontSize = 12,
|
||||
TextColor = Colors.Gray
|
||||
});
|
||||
|
||||
scroll.Content = root;
|
||||
@@ -350,14 +472,14 @@ public static class LinuxProgramHost
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 18,
|
||||
TextColor = new SKColor(0x37, 0x47, 0x4F),
|
||||
IsBold = true
|
||||
TextColor = Color.FromRgb(0x37, 0x47, 0x4F),
|
||||
FontAttributes = FontAttributes.Bold
|
||||
};
|
||||
}
|
||||
|
||||
private static SkiaView CreateSeparator()
|
||||
{
|
||||
var sep = new SkiaLabel { Text = "", BackgroundColor = new SKColor(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
|
||||
var sep = new SkiaLabel { Text = "", BackgroundColor = Color.FromRgb(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
|
||||
return sep;
|
||||
}
|
||||
|
||||
|
||||
47
Hosting/LinuxTicker.cs
Normal file
47
Hosting/LinuxTicker.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
564
Hosting/LinuxViewRenderer.cs
Normal file
564
Hosting/LinuxViewRenderer.cs
Normal file
@@ -0,0 +1,564 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Renders MAUI views to Skia platform views.
|
||||
/// Handles the conversion of the view hierarchy.
|
||||
/// </summary>
|
||||
public class LinuxViewRenderer
|
||||
{
|
||||
private readonly IMauiContext _mauiContext;
|
||||
|
||||
/// <summary>
|
||||
/// Static reference to the current MAUI Shell for navigation support.
|
||||
/// Used when Shell.Current is not available through normal lifecycle.
|
||||
/// </summary>
|
||||
public static Shell? CurrentMauiShell { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Static reference to the current SkiaShell for navigation updates.
|
||||
/// </summary>
|
||||
public static SkiaShell? CurrentSkiaShell { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a route using the SkiaShell directly.
|
||||
/// Use this instead of Shell.Current.GoToAsync on Linux.
|
||||
/// </summary>
|
||||
/// <param name="route">The route to navigate to (e.g., "Buttons" or "//Buttons")</param>
|
||||
/// <returns>True if navigation succeeded</returns>
|
||||
public static bool NavigateToRoute(string route)
|
||||
{
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up the route - remove leading // or /
|
||||
var cleanRoute = route.TrimStart('/');
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Navigating to: {cleanRoute}");
|
||||
|
||||
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
|
||||
{
|
||||
var section = CurrentSkiaShell.Sections[i];
|
||||
if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) ||
|
||||
section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Found section {i}: {section.Title}");
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", $"NavigateToRoute: Route not found: {cleanRoute}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current renderer instance for page rendering.
|
||||
/// </summary>
|
||||
public static LinuxViewRenderer? CurrentRenderer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a page onto the navigation stack.
|
||||
/// </summary>
|
||||
/// <param name="page">The page to push</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool PushPage(Page page)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"PushPage: Pushing page: {page.GetType().Name}");
|
||||
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CurrentRenderer == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentRenderer is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Render the page through the proper handler system
|
||||
// This ensures all properties (including BackgroundColor via AppThemeBinding) are mapped
|
||||
var skiaPage = CurrentRenderer.RenderPage(page);
|
||||
|
||||
if (skiaPage == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: Failed to render page through handler");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Push onto SkiaShell's navigation stack
|
||||
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", "PushPage: Successfully pushed page via handler system");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("LinuxViewRenderer", "PushPage failed", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops the current page from the navigation stack.
|
||||
/// </summary>
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool PopPage()
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", "PopPage: Popping page");
|
||||
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PopPage: CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
return CurrentSkiaShell.PopAsync();
|
||||
}
|
||||
|
||||
public LinuxViewRenderer(IMauiContext mauiContext)
|
||||
{
|
||||
_mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext));
|
||||
// Store reference for push/pop navigation
|
||||
CurrentRenderer = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a MAUI page and returns the corresponding SkiaView.
|
||||
/// </summary>
|
||||
public SkiaView? RenderPage(Page page)
|
||||
{
|
||||
if (page == null)
|
||||
return null;
|
||||
|
||||
// Special handling for Shell - Shell is our navigation container
|
||||
if (page is Shell shell)
|
||||
{
|
||||
return RenderShell(shell);
|
||||
}
|
||||
|
||||
// Set handler context
|
||||
page.Handler?.DisconnectHandler();
|
||||
var handler = page.ToHandler(_mauiContext);
|
||||
|
||||
// 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)
|
||||
{
|
||||
return skiaPage;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a MAUI Shell with all its navigation structure.
|
||||
/// </summary>
|
||||
private SkiaShell RenderShell(Shell shell)
|
||||
{
|
||||
// Store reference for navigation - Shell.Current is computed from Application.Current.Windows
|
||||
// Our platform handles navigation through SkiaShell directly
|
||||
CurrentMauiShell = shell;
|
||||
|
||||
var skiaShell = new SkiaShell
|
||||
{
|
||||
Title = shell.Title ?? "App",
|
||||
FlyoutBehavior = shell.FlyoutBehavior switch
|
||||
{
|
||||
FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout,
|
||||
FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
|
||||
FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
|
||||
_ => ShellFlyoutBehavior.Flyout
|
||||
},
|
||||
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
|
||||
foreach (var item in shell.Items)
|
||||
{
|
||||
ProcessShellItem(skiaShell, item);
|
||||
}
|
||||
|
||||
// Store reference to SkiaShell for navigation
|
||||
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
|
||||
shell.Navigated += OnShellNavigated;
|
||||
shell.Navigating += (s, e) => DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Navigating: {e.Target}");
|
||||
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
|
||||
for (int i = 0; i < skiaShell.Sections.Count; i++)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'");
|
||||
}
|
||||
|
||||
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>
|
||||
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
|
||||
/// </summary>
|
||||
private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}");
|
||||
|
||||
if (CurrentSkiaShell == null || CurrentMauiShell == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell or CurrentMauiShell is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current route from the Shell
|
||||
var currentState = CurrentMauiShell.CurrentState;
|
||||
var location = currentState?.Location?.OriginalString ?? "";
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
|
||||
|
||||
// Find the matching section in SkiaShell by route
|
||||
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
|
||||
{
|
||||
var section = CurrentSkiaShell.Sections[i];
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
|
||||
if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by route! Navigating to section {i}");
|
||||
if (i != CurrentSkiaShell.CurrentSectionIndex)
|
||||
{
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by title! Navigating to section {i}");
|
||||
if (i != CurrentSkiaShell.CurrentSectionIndex)
|
||||
{
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", $"Navigation: No matching section found for location: {location}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections.
|
||||
/// </summary>
|
||||
private void ProcessShellItem(SkiaShell skiaShell, ShellItem item)
|
||||
{
|
||||
if (item is FlyoutItem flyoutItem)
|
||||
{
|
||||
// Each FlyoutItem becomes a section
|
||||
var section = new ShellSection
|
||||
{
|
||||
Title = flyoutItem.Title ?? "",
|
||||
Route = flyoutItem.Route ?? flyoutItem.Title ?? ""
|
||||
};
|
||||
|
||||
// Process the items within the FlyoutItem
|
||||
foreach (var shellSection in flyoutItem.Items)
|
||||
{
|
||||
foreach (var content in shellSection.Items)
|
||||
{
|
||||
var shellContent = new ShellContent
|
||||
{
|
||||
Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "",
|
||||
Route = content.Route ?? "",
|
||||
MauiShellContent = content
|
||||
};
|
||||
|
||||
// Create the page content
|
||||
var pageContent = CreateShellContentPage(content);
|
||||
if (pageContent != null)
|
||||
{
|
||||
shellContent.Content = pageContent;
|
||||
}
|
||||
|
||||
section.Items.Add(shellContent);
|
||||
}
|
||||
}
|
||||
|
||||
// If there's only one item, use it as the main section content
|
||||
if (section.Items.Count == 1)
|
||||
{
|
||||
section.Title = section.Items[0].Title;
|
||||
}
|
||||
|
||||
skiaShell.AddSection(section);
|
||||
}
|
||||
else if (item is TabBar tabBar)
|
||||
{
|
||||
// TabBar items get their own sections
|
||||
foreach (var tab in tabBar.Items)
|
||||
{
|
||||
var section = new ShellSection
|
||||
{
|
||||
Title = tab.Title ?? "",
|
||||
Route = tab.Route ?? ""
|
||||
};
|
||||
|
||||
foreach (var content in tab.Items)
|
||||
{
|
||||
var shellContent = new ShellContent
|
||||
{
|
||||
Title = content.Title ?? tab.Title ?? "",
|
||||
Route = content.Route ?? "",
|
||||
MauiShellContent = content
|
||||
};
|
||||
|
||||
var pageContent = CreateShellContentPage(content);
|
||||
if (pageContent != null)
|
||||
{
|
||||
shellContent.Content = pageContent;
|
||||
}
|
||||
|
||||
section.Items.Add(shellContent);
|
||||
}
|
||||
|
||||
skiaShell.AddSection(section);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic ShellItem
|
||||
var section = new ShellSection
|
||||
{
|
||||
Title = item.Title ?? "",
|
||||
Route = item.Route ?? ""
|
||||
};
|
||||
|
||||
foreach (var shellSection in item.Items)
|
||||
{
|
||||
foreach (var content in shellSection.Items)
|
||||
{
|
||||
var shellContent = new ShellContent
|
||||
{
|
||||
Title = content.Title ?? "",
|
||||
Route = content.Route ?? "",
|
||||
MauiShellContent = content
|
||||
};
|
||||
|
||||
var pageContent = CreateShellContentPage(content);
|
||||
if (pageContent != null)
|
||||
{
|
||||
shellContent.Content = pageContent;
|
||||
}
|
||||
|
||||
section.Items.Add(shellContent);
|
||||
}
|
||||
}
|
||||
|
||||
skiaShell.AddSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the page content for a ShellContent.
|
||||
/// </summary>
|
||||
private SkiaView? CreateShellContentPage(Controls.ShellContent content)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to create the page from the content template
|
||||
Page? page = null;
|
||||
|
||||
if (content.ContentTemplate != null)
|
||||
{
|
||||
page = content.ContentTemplate.CreateContent() as Page;
|
||||
}
|
||||
|
||||
if (page == null && content.Content is Page contentPage)
|
||||
{
|
||||
page = contentPage;
|
||||
}
|
||||
|
||||
if (page is ContentPage cp && cp.Content != null)
|
||||
{
|
||||
// Wrap in a scroll view if not already scrollable
|
||||
var contentView = RenderView(cp.Content);
|
||||
if (contentView != null)
|
||||
{
|
||||
// Get page background color if set
|
||||
Color? bgColor = null;
|
||||
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
|
||||
{
|
||||
bgColor = cp.BackgroundColor;
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"CreateShellContentPage: Page BackgroundColor: {bgColor}");
|
||||
}
|
||||
|
||||
if (contentView is SkiaScrollView scrollView)
|
||||
{
|
||||
if (bgColor != null)
|
||||
{
|
||||
scrollView.BackgroundColor = bgColor;
|
||||
}
|
||||
return scrollView;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newScrollView = new SkiaScrollView
|
||||
{
|
||||
Content = contentView
|
||||
};
|
||||
if (bgColor != null)
|
||||
{
|
||||
newScrollView.BackgroundColor = bgColor;
|
||||
}
|
||||
return newScrollView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently handle template creation errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a MAUI view and returns the corresponding SkiaView.
|
||||
/// </summary>
|
||||
public SkiaView? RenderView(IView view)
|
||||
{
|
||||
if (view == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Disconnect any existing handler
|
||||
if (view is Element element && element.Handler != null)
|
||||
{
|
||||
element.Handler.DisconnectHandler();
|
||||
}
|
||||
|
||||
// Create handler for the view
|
||||
// The handler's ConnectHandler and property mappers handle child views automatically
|
||||
var handler = view.ToHandler(_mauiContext);
|
||||
|
||||
if (handler?.PlatformView is not SkiaView skiaView)
|
||||
{
|
||||
// If no Skia handler, create a fallback
|
||||
return CreateFallbackView(view);
|
||||
}
|
||||
|
||||
// Handlers manage their own children via ConnectHandler and property mappers
|
||||
// No manual child rendering needed here - that caused "View already has a parent" errors
|
||||
return skiaView;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return CreateFallbackView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fallback view for unsupported view types.
|
||||
/// </summary>
|
||||
private SkiaView CreateFallbackView(IView view)
|
||||
{
|
||||
// For views without handlers, create a placeholder
|
||||
return new SkiaLabel
|
||||
{
|
||||
Text = $"[{view.GetType().Name}]",
|
||||
TextColor = Colors.Gray,
|
||||
FontSize = 12
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
124
Hosting/MauiHandlerExtensions.cs
Normal file
124
Hosting/MauiHandlerExtensions.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
Hosting/ScopedLinuxMauiContext.cs
Normal file
18
Hosting/ScopedLinuxMauiContext.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
@@ -153,4 +153,154 @@ public static class KeyMapping
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
// Linux evdev keycode to Key mapping (used by Wayland)
|
||||
private static readonly Dictionary<uint, Key> LinuxKeycodeToKey = new()
|
||||
{
|
||||
// Top row
|
||||
[1] = Key.Escape,
|
||||
[2] = Key.D1, [3] = Key.D2, [4] = Key.D3, [5] = Key.D4, [6] = Key.D5,
|
||||
[7] = Key.D6, [8] = Key.D7, [9] = Key.D8, [10] = Key.D9, [11] = Key.D0,
|
||||
[12] = Key.Minus, [13] = Key.Equals, [14] = Key.Backspace, [15] = Key.Tab,
|
||||
|
||||
// QWERTY row
|
||||
[16] = Key.Q, [17] = Key.W, [18] = Key.E, [19] = Key.R, [20] = Key.T,
|
||||
[21] = Key.Y, [22] = Key.U, [23] = Key.I, [24] = Key.O, [25] = Key.P,
|
||||
[26] = Key.LeftBracket, [27] = Key.RightBracket, [28] = Key.Enter,
|
||||
|
||||
// Control and ASDF row
|
||||
[29] = Key.Control,
|
||||
[30] = Key.A, [31] = Key.S, [32] = Key.D, [33] = Key.F, [34] = Key.G,
|
||||
[35] = Key.H, [36] = Key.J, [37] = Key.K, [38] = Key.L,
|
||||
[39] = Key.Semicolon, [40] = Key.Quote, [41] = Key.Grave,
|
||||
|
||||
// Shift and ZXCV row
|
||||
[42] = Key.Shift, [43] = Key.Backslash,
|
||||
[44] = Key.Z, [45] = Key.X, [46] = Key.C, [47] = Key.V, [48] = Key.B,
|
||||
[49] = Key.N, [50] = Key.M,
|
||||
[51] = Key.Comma, [52] = Key.Period, [53] = Key.Slash, [54] = Key.Shift,
|
||||
|
||||
// Bottom row
|
||||
[55] = Key.NumPadMultiply, [56] = Key.Alt, [57] = Key.Space,
|
||||
[58] = Key.CapsLock,
|
||||
|
||||
// Function keys
|
||||
[59] = Key.F1, [60] = Key.F2, [61] = Key.F3, [62] = Key.F4,
|
||||
[63] = Key.F5, [64] = Key.F6, [65] = Key.F7, [66] = Key.F8,
|
||||
[67] = Key.F9, [68] = Key.F10,
|
||||
|
||||
// NumLock and numpad
|
||||
[69] = Key.NumLock, [70] = Key.ScrollLock,
|
||||
[71] = Key.NumPad7, [72] = Key.NumPad8, [73] = Key.NumPad9, [74] = Key.NumPadSubtract,
|
||||
[75] = Key.NumPad4, [76] = Key.NumPad5, [77] = Key.NumPad6, [78] = Key.NumPadAdd,
|
||||
[79] = Key.NumPad1, [80] = Key.NumPad2, [81] = Key.NumPad3,
|
||||
[82] = Key.NumPad0, [83] = Key.NumPadDecimal,
|
||||
|
||||
// More function keys
|
||||
[87] = Key.F11, [88] = Key.F12,
|
||||
|
||||
// Extended keys
|
||||
[96] = Key.Enter, // NumPad Enter
|
||||
[97] = Key.Control, // Right Control
|
||||
[98] = Key.NumPadDivide,
|
||||
[99] = Key.PrintScreen,
|
||||
[100] = Key.Alt, // Right Alt
|
||||
[102] = Key.Home,
|
||||
[103] = Key.Up,
|
||||
[104] = Key.PageUp,
|
||||
[105] = Key.Left,
|
||||
[106] = Key.Right,
|
||||
[107] = Key.End,
|
||||
[108] = Key.Down,
|
||||
[109] = Key.PageDown,
|
||||
[110] = Key.Insert,
|
||||
[111] = Key.Delete,
|
||||
[119] = Key.Pause,
|
||||
[125] = Key.Super, // Left Super (Windows key)
|
||||
[126] = Key.Super, // Right Super
|
||||
[127] = Key.Menu,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Linux evdev keycode to a MAUI Key.
|
||||
/// Used for Wayland input where keycodes are offset by 8 from X11 keycodes.
|
||||
/// </summary>
|
||||
public static Key FromLinuxKeycode(uint keycode)
|
||||
{
|
||||
// Wayland uses evdev keycodes, X11 uses keycodes + 8
|
||||
// If caller added 8, subtract it
|
||||
var evdevCode = keycode >= 8 ? keycode - 8 : keycode;
|
||||
|
||||
if (LinuxKeycodeToKey.TryGetValue(evdevCode, out var key))
|
||||
return key;
|
||||
|
||||
return Key.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Key to its character representation, if applicable.
|
||||
/// </summary>
|
||||
public static char? ToChar(Key key, KeyModifiers modifiers)
|
||||
{
|
||||
bool shift = modifiers.HasFlag(KeyModifiers.Shift);
|
||||
bool capsLock = modifiers.HasFlag(KeyModifiers.CapsLock);
|
||||
bool upper = shift ^ capsLock;
|
||||
|
||||
// Letters
|
||||
if (key >= Key.A && key <= Key.Z)
|
||||
{
|
||||
char ch = (char)('a' + (key - Key.A));
|
||||
return upper ? char.ToUpper(ch) : ch;
|
||||
}
|
||||
|
||||
// Numbers (with shift gives symbols)
|
||||
if (key >= Key.D0 && key <= Key.D9)
|
||||
{
|
||||
if (shift)
|
||||
{
|
||||
return (key - Key.D0) switch
|
||||
{
|
||||
0 => ')',
|
||||
1 => '!',
|
||||
2 => '@',
|
||||
3 => '#',
|
||||
4 => '$',
|
||||
5 => '%',
|
||||
6 => '^',
|
||||
7 => '&',
|
||||
8 => '*',
|
||||
9 => '(',
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
return (char)('0' + (key - Key.D0));
|
||||
}
|
||||
|
||||
// NumPad numbers
|
||||
if (key >= Key.NumPad0 && key <= Key.NumPad9)
|
||||
return (char)('0' + (key - Key.NumPad0));
|
||||
|
||||
// Punctuation
|
||||
return key switch
|
||||
{
|
||||
Key.Space => ' ',
|
||||
Key.Comma => shift ? '<' : ',',
|
||||
Key.Period => shift ? '>' : '.',
|
||||
Key.Slash => shift ? '?' : '/',
|
||||
Key.Semicolon => shift ? ':' : ';',
|
||||
Key.Quote => shift ? '"' : '\'',
|
||||
Key.LeftBracket => shift ? '{' : '[',
|
||||
Key.RightBracket => shift ? '}' : ']',
|
||||
Key.Backslash => shift ? '|' : '\\',
|
||||
Key.Minus => shift ? '_' : '-',
|
||||
Key.Equals => shift ? '+' : '=',
|
||||
Key.Grave => shift ? '~' : '`',
|
||||
Key.NumPadAdd => '+',
|
||||
Key.NumPadSubtract => '-',
|
||||
Key.NumPadMultiply => '*',
|
||||
Key.NumPadDivide => '/',
|
||||
Key.NumPadDecimal => '.',
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
25
Interop/ClientMessageData.cs
Normal file
25
Interop/ClientMessageData.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
}
|
||||
345
Interop/WebKitGtk.cs
Normal file
345
Interop/WebKitGtk.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
// 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
|
||||
}
|
||||
239
Interop/X11.cs
Normal file
239
Interop/X11.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,482 +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 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
|
||||
23
Interop/XButtonEvent.cs
Normal file
23
Interop/XButtonEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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;
|
||||
}
|
||||
13
Interop/XClassHint.cs
Normal file
13
Interop/XClassHint.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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;
|
||||
}
|
||||
16
Interop/XClientMessageEvent.cs
Normal file
16
Interop/XClientMessageEvent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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;
|
||||
}
|
||||
21
Interop/XConfigureEvent.cs
Normal file
21
Interop/XConfigureEvent.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user