Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||
|
|
071df4eb28 | ||
|
|
2535f16893 | ||
|
|
924d0ae874 | ||
|
|
7172907333 | ||
|
|
a4d9d2c1eb |
46
.gitea/workflows/ci.yml
Normal file
46
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# OpenMaui Linux CI/CD Pipeline for Gitea
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
DOTNET_ROOT: C:\dotnet
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and Test
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: C:\dotnet\dotnet.exe restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Pack NuGet (preview)
|
||||||
|
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
|
||||||
|
|
||||||
|
- name: List NuGet packages
|
||||||
|
run: dir .\nupkg\
|
||||||
|
|
||||||
|
- name: Push to NuGet.org
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
foreach ($pkg in (Get-ChildItem .\nupkg\*.nupkg)) {
|
||||||
|
C:\dotnet\dotnet.exe nuget push $pkg.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||||
41
.gitea/workflows/release.yml
Normal file
41
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# OpenMaui Linux Release - Publish to NuGet
|
||||||
|
name: Release to NuGet
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
DOTNET_ROOT: C:\dotnet
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Publish to NuGet
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: C:\dotnet\dotnet.exe restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Pack NuGet package
|
||||||
|
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
|
||||||
|
|
||||||
|
- name: Publish to NuGet.org
|
||||||
|
run: |
|
||||||
|
foreach ($pkg in (Get-ChildItem .\nupkg\*.nupkg)) {
|
||||||
|
C:\dotnet\dotnet.exe nuget push $pkg.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||||
|
}
|
||||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
@@ -42,14 +42,14 @@ jobs:
|
|||||||
run: dotnet test --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx"
|
run: dotnet test --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx"
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: '**/TestResults/*.trx'
|
path: '**/TestResults/*.trx'
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: |
|
path: |
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
run: dotnet pack --configuration Release --no-restore -o ./nupkg
|
run: dotnet pack --configuration Release --no-restore -o ./nupkg
|
||||||
|
|
||||||
- name: Upload NuGet package
|
- name: Upload NuGet package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: nuget-package
|
name: nuget-package
|
||||||
path: ./nupkg/*.nupkg
|
path: ./nupkg/*.nupkg
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
run: dotnet pack --configuration Release --no-build -o ./nupkg /p:PackageVersion=${{ steps.version.outputs.version }}
|
run: dotnet pack --configuration Release --no-build -o ./nupkg /p:PackageVersion=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
- name: Upload NuGet package artifact
|
- name: Upload NuGet package artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: nuget-package-${{ steps.version.outputs.version }}
|
name: nuget-package-${{ steps.version.outputs.version }}
|
||||||
path: ./nupkg/*.nupkg
|
path: ./nupkg/*.nupkg
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attach packages to release
|
- name: Attach packages to release
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: ./nupkg/*.nupkg
|
files: ./nupkg/*.nupkg
|
||||||
env:
|
env:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ coverage*.xml
|
|||||||
|
|
||||||
# Publish output
|
# Publish output
|
||||||
publish/
|
publish/
|
||||||
|
mauiplan.md
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
CLAUDE.md
Normal file
297
CLAUDE.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# CLAUDE.md - OpenMaui XAML Reconstruction
|
||||||
|
|
||||||
|
## CURRENT TASK: Reconstruct XAML from Decompiled Code
|
||||||
|
|
||||||
|
The sample applications (ShellDemo, TodoApp, XamlBrowser) were recovered from decompiled DLLs. The XAML files were compiled away - we have only the generated `InitializeComponent()` code. **Screenshots will be provided** to help verify visual accuracy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Locations
|
||||||
|
|
||||||
|
| What | Path |
|
||||||
|
|------|------|
|
||||||
|
| **Main codebase** | `/Users/nible/Documents/GitHub/maui-linux-main/` |
|
||||||
|
| **Samples (target)** | `/Users/nible/Documents/GitHub/maui-linux-main/samples_temp/` |
|
||||||
|
| **Decompiled samples** | `/Users/nible/Documents/GitHub/recovered/source/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Branch
|
||||||
|
|
||||||
|
**Work on `final` branch.** Commit frequently.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch # Should show: * final
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## XAML Reconstruction Overview
|
||||||
|
|
||||||
|
### ShellDemo (10 pages + shell + app)
|
||||||
|
|
||||||
|
| File | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| App.xaml | [ ] | Colors, Styles (ThemedEntry, TitleLabel, etc.) |
|
||||||
|
| AppShell.xaml | [ ] | Shell with FlyoutHeader, 9 FlyoutItems |
|
||||||
|
| HomePage.xaml | [ ] | Welcome screen with logo |
|
||||||
|
| ButtonsPage.xaml | [ ] | Button demos |
|
||||||
|
| TextInputPage.xaml | [ ] | Entry/Editor demos |
|
||||||
|
| SelectionPage.xaml | [ ] | CheckBox, Switch, RadioButton demos |
|
||||||
|
| PickersPage.xaml | [ ] | DatePicker, TimePicker, Picker demos |
|
||||||
|
| ListsPage.xaml | [ ] | CollectionView demos |
|
||||||
|
| ProgressPage.xaml | [ ] | ProgressBar, ActivityIndicator demos |
|
||||||
|
| GridsPage.xaml | [ ] | Grid layout demos |
|
||||||
|
| AboutPage.xaml | [ ] | About information |
|
||||||
|
| DetailPage.xaml | [ ] | Navigation detail page |
|
||||||
|
|
||||||
|
### TodoApp (app + 3 pages)
|
||||||
|
|
||||||
|
| File | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| App.xaml | [ ] | Colors, Icon strings |
|
||||||
|
| TodoListPage.xaml | [ ] | Main list with swipe actions |
|
||||||
|
| NewTodoPage.xaml | [ ] | Add new todo form |
|
||||||
|
| TodoDetailPage.xaml | [ ] | Edit todo details |
|
||||||
|
|
||||||
|
### XamlBrowser (app + 1 page) - COMPLETE
|
||||||
|
|
||||||
|
| File | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| App.xaml | [x] | Colors, styles (NavButtonStyle, GoButtonStyle, AddressBarStyle, StatusLabelStyle) |
|
||||||
|
| App.xaml.cs | [x] | BrowserApp with ToggleTheme() |
|
||||||
|
| MainPage.xaml | [x] | Toolbar with nav buttons, address bar, WebView, status bar |
|
||||||
|
| MainPage.xaml.cs | [x] | Navigation logic, progress animation, theme toggle |
|
||||||
|
| MauiProgram.cs | [x] | UseLinuxPlatform() setup |
|
||||||
|
| Program.cs | [x] | LinuxProgramHost entry point |
|
||||||
|
| Resources/Images/*.svg | [x] | 10 toolbar icons (dark/light variants) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Reconstruct XAML
|
||||||
|
|
||||||
|
### Step 1: Read the decompiled InitializeComponent()
|
||||||
|
|
||||||
|
Look for patterns like:
|
||||||
|
```csharp
|
||||||
|
// Setting a property
|
||||||
|
((BindableObject)val8).SetValue(Label.TextProperty, (object)"OpenMaui");
|
||||||
|
|
||||||
|
// AppThemeBinding (light/dark mode)
|
||||||
|
val7.Light = "White";
|
||||||
|
val7.Dark = "#E0E0E0";
|
||||||
|
|
||||||
|
// StaticResource
|
||||||
|
val.Key = "PrimaryColor";
|
||||||
|
|
||||||
|
// Layout hierarchy
|
||||||
|
((Layout)val12).Children.Add((IView)(object)val6);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Convert to XAML
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// This C#:
|
||||||
|
((BindableObject)val8).SetValue(Label.TextProperty, (object)"OpenMaui");
|
||||||
|
((BindableObject)val8).SetValue(Label.FontSizeProperty, (object)22.0);
|
||||||
|
((BindableObject)val8).SetValue(Label.FontAttributesProperty, (object)(FontAttributes)1);
|
||||||
|
val7.Light = "White";
|
||||||
|
val7.Dark = "#E0E0E0";
|
||||||
|
((BindableObject)val8).SetBinding(Label.TextColorProperty, val74);
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Becomes this XAML: -->
|
||||||
|
<Label Text="OpenMaui"
|
||||||
|
FontSize="22"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
TextColor="{AppThemeBinding Light=White, Dark=#E0E0E0}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Verify with screenshots (when provided)
|
||||||
|
|
||||||
|
Compare the reconstructed XAML against the actual screenshots to ensure visual fidelity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App.xaml Resources Reference
|
||||||
|
|
||||||
|
### ShellDemo Colors (extracted from decompiled)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Light theme -->
|
||||||
|
<Color x:Key="PrimaryColor">#2196F3</Color>
|
||||||
|
<Color x:Key="PrimaryDarkColor">#1976D2</Color>
|
||||||
|
<Color x:Key="AccentColor">#FF4081</Color>
|
||||||
|
<Color x:Key="PageBackgroundLight">#F8F8F8</Color>
|
||||||
|
<Color x:Key="CardBackgroundLight">#FFFFFF</Color>
|
||||||
|
<Color x:Key="TextPrimaryLight">#212121</Color>
|
||||||
|
<Color x:Key="TextSecondaryLight">#757575</Color>
|
||||||
|
<Color x:Key="BorderLight">#E0E0E0</Color>
|
||||||
|
<Color x:Key="EntryBackgroundLight">#F9F9F9</Color>
|
||||||
|
<Color x:Key="ShellBackgroundLight">#FFFFFF</Color>
|
||||||
|
<Color x:Key="FlyoutBackgroundLight">#FFFFFF</Color>
|
||||||
|
<Color x:Key="ProgressTrackLight">#E0E0E0</Color>
|
||||||
|
|
||||||
|
<!-- Dark theme -->
|
||||||
|
<Color x:Key="PageBackgroundDark">#121212</Color>
|
||||||
|
<Color x:Key="CardBackgroundDark">#1E1E1E</Color>
|
||||||
|
<Color x:Key="TextPrimaryDark">#FFFFFF</Color>
|
||||||
|
<Color x:Key="TextSecondaryDark">#B0B0B0</Color>
|
||||||
|
<Color x:Key="BorderDark">#424242</Color>
|
||||||
|
<Color x:Key="EntryBackgroundDark">#2C2C2C</Color>
|
||||||
|
<Color x:Key="ShellBackgroundDark">#1E1E1E</Color>
|
||||||
|
<Color x:Key="FlyoutBackgroundDark">#1E1E1E</Color>
|
||||||
|
<Color x:Key="ProgressTrackDark">#424242</Color>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ShellDemo Styles (extracted from decompiled)
|
||||||
|
|
||||||
|
- **ThemedEntry**: BackgroundColor, TextColor, PlaceholderColor with AppThemeBinding
|
||||||
|
- **ThemedEditor**: BackgroundColor, TextColor, PlaceholderColor with AppThemeBinding
|
||||||
|
- **TitleLabel**: FontSize=24, FontAttributes=Bold, TextColor with AppThemeBinding
|
||||||
|
- **SubtitleLabel**: FontSize=16, TextColor with AppThemeBinding
|
||||||
|
- **ThemedFrame**: BackgroundColor, BorderColor with AppThemeBinding
|
||||||
|
- **ThemedProgressBar**: ProgressColor=PrimaryColor, BackgroundColor with AppThemeBinding
|
||||||
|
- **PrimaryButton**: BackgroundColor=PrimaryColor, TextColor=White
|
||||||
|
- **SecondaryButton**: Light/dark themed background and text
|
||||||
|
|
||||||
|
### TodoApp Colors
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Color x:Key="PrimaryColor">#5C6BC0</Color>
|
||||||
|
<Color x:Key="PrimaryDarkColor">#3949AB</Color>
|
||||||
|
<Color x:Key="AccentColor">#26A69A</Color>
|
||||||
|
<Color x:Key="DangerColor">#EF5350</Color>
|
||||||
|
<!-- ... plus light/dark theme colors -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### TodoApp Icons (Material Design)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<x:String x:Key="IconAdd"></x:String>
|
||||||
|
<x:String x:Key="IconDelete"></x:String>
|
||||||
|
<x:String x:Key="IconSave"></x:String>
|
||||||
|
<x:String x:Key="IconCheck"></x:String>
|
||||||
|
<x:String x:Key="IconEdit"></x:String>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AppShell.xaml Structure (ShellDemo)
|
||||||
|
|
||||||
|
From decompiled code, the shell has:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Shell Title="OpenMaui Controls Demo"
|
||||||
|
FlyoutBehavior="Flyout"
|
||||||
|
FlyoutBackgroundColor="{AppThemeBinding Light={StaticResource FlyoutBackgroundLight}, Dark={StaticResource FlyoutBackgroundDark}}">
|
||||||
|
|
||||||
|
<!-- FlyoutHeader: Grid with logo and title -->
|
||||||
|
<Shell.FlyoutHeader>
|
||||||
|
<Grid BackgroundColor="{AppThemeBinding ...}" HeightRequest="140" Padding="15">
|
||||||
|
<HorizontalStackLayout VerticalOptions="Center" Spacing="12">
|
||||||
|
<Image Source="openmaui_logo.svg" WidthRequest="60" HeightRequest="60" />
|
||||||
|
<VerticalStackLayout VerticalOptions="Center">
|
||||||
|
<Label Text="OpenMaui" FontSize="22" FontAttributes="Bold"
|
||||||
|
TextColor="{AppThemeBinding Light=White, Dark=#E0E0E0}" />
|
||||||
|
<Label Text="Controls Demo" FontSize="13" Opacity="0.9"
|
||||||
|
TextColor="{AppThemeBinding Light=White, Dark=#B0B0B0}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
</Shell.FlyoutHeader>
|
||||||
|
|
||||||
|
<!-- FlyoutItems with emoji icons -->
|
||||||
|
<FlyoutItem Title="Home" Route="Home">
|
||||||
|
<FlyoutItem.Icon><FontImageSource Glyph="🏠" FontFamily="Default" Color="{AppThemeBinding ...}" /></FlyoutItem.Icon>
|
||||||
|
<ShellContent ContentTemplate="{DataTemplate local:HomePage}" />
|
||||||
|
</FlyoutItem>
|
||||||
|
|
||||||
|
<FlyoutItem Title="Buttons" Route="Buttons">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Text Input" Route="TextInput">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Selection" Route="Selection">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Pickers" Route="Pickers">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Lists" Route="Lists">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Progress" Route="Progress">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Grids" Route="Grids">...</FlyoutItem>
|
||||||
|
<FlyoutItem Title="About" Route="About">...</FlyoutItem>
|
||||||
|
</Shell>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decompiled File Locations
|
||||||
|
|
||||||
|
| Sample | Decompiled Path |
|
||||||
|
|--------|-----------------|
|
||||||
|
| ShellDemo | `/Users/nible/Documents/GitHub/recovered/source/ShellDemo/ShellDemo/` |
|
||||||
|
| TodoApp | `/Users/nible/Documents/GitHub/recovered/source/TodoApp/TodoApp/` |
|
||||||
|
| XamlBrowser | `/Users/nible/Documents/GitHub/recovered/source/XamlBrowser/XamlBrowser/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/nible/Documents/GitHub/maui-linux-main
|
||||||
|
dotnet build OpenMaui.Controls.Linux.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Patterns in Decompiled Code
|
||||||
|
|
||||||
|
### 1. Color Values
|
||||||
|
```csharp
|
||||||
|
Color val = new Color(11f / 85f, 0.5882353f, 81f / 85f, 1f);
|
||||||
|
// = Color.FromRgba(0.129, 0.588, 0.953, 1.0) = #2196F3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. AppThemeBinding
|
||||||
|
```csharp
|
||||||
|
AppThemeBindingExtension val7 = new AppThemeBindingExtension();
|
||||||
|
val7.Light = "White";
|
||||||
|
val7.Dark = "#E0E0E0";
|
||||||
|
```
|
||||||
|
Becomes: `{AppThemeBinding Light=White, Dark=#E0E0E0}`
|
||||||
|
|
||||||
|
### 3. StaticResource
|
||||||
|
```csharp
|
||||||
|
val.Key = "PrimaryColor";
|
||||||
|
```
|
||||||
|
Becomes: `{StaticResource PrimaryColor}`
|
||||||
|
|
||||||
|
### 4. Layout Hierarchy
|
||||||
|
```csharp
|
||||||
|
((Layout)val12).Children.Add((IView)(object)val6);
|
||||||
|
((Layout)val12).Children.Add((IView)(object)val11);
|
||||||
|
```
|
||||||
|
The children are added in order - first child is val6, second is val11.
|
||||||
|
|
||||||
|
### 5. FontAttributes Enum
|
||||||
|
```csharp
|
||||||
|
(FontAttributes)1 // Bold
|
||||||
|
(FontAttributes)2 // Italic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow for Each File
|
||||||
|
|
||||||
|
1. **Read decompiled** `InitializeComponent()` method
|
||||||
|
2. **Extract** all UI elements and their properties
|
||||||
|
3. **Write XAML** with proper structure
|
||||||
|
4. **Create code-behind** (usually just constructor calling InitializeComponent)
|
||||||
|
5. **Verify** against screenshot if available
|
||||||
|
6. **Update tracking** in this file
|
||||||
|
7. **Commit** with descriptive message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The decompiled code has ALL the information needed - it's just in C# form instead of XAML
|
||||||
|
- Screenshots will help verify visual accuracy
|
||||||
|
- Focus on one file at a time
|
||||||
|
- Commit after each completed file
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Dispatching/LinuxDispatcher.cs
Normal file
76
Dispatching/LinuxDispatcher.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Maui.Dispatching;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Native;
|
||||||
|
|
||||||
|
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();
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[LinuxDispatcher] Error in dispatched action: " + ex.Message);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[LinuxDispatcher] Error in delayed action: " + ex.Message);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Dispatching/LinuxDispatcherTimer.cs
Normal file
109
Dispatching/LinuxDispatcherTimer.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Maui.Dispatching;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Native;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[LinuxDispatcherTimer] Error in Tick handler: " + ex.Message);
|
||||||
|
}
|
||||||
|
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.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using Microsoft.Maui.Platform;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
|||||||
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
|
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
|
||||||
[nameof(IActivityIndicator.Color)] = MapColor,
|
[nameof(IActivityIndicator.Color)] = MapColor,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -38,6 +39,19 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
|||||||
return new SkiaActivityIndicator();
|
return new SkiaActivityIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaActivityIndicator platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
// Sync properties
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
MapIsRunning(this, VirtualView);
|
||||||
|
MapColor(this, VirtualView);
|
||||||
|
MapIsEnabled(this, VirtualView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
@@ -49,7 +63,7 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (activityIndicator.Color is not null)
|
if (activityIndicator.Color is not null)
|
||||||
handler.PlatformView.Color = activityIndicator.Color.ToSKColor();
|
handler.PlatformView.Color = activityIndicator.Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
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)
|
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,7 @@ using Microsoft.Maui.Handlers;
|
|||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -20,8 +21,17 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
[nameof(IBorderView.Content)] = MapContent,
|
[nameof(IBorderView.Content)] = MapContent,
|
||||||
[nameof(IBorderStroke.Stroke)] = MapStroke,
|
[nameof(IBorderStroke.Stroke)] = MapStroke,
|
||||||
[nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness,
|
[nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness,
|
||||||
|
["StrokeDashArray"] = MapStrokeDashArray,
|
||||||
|
["StrokeDashOffset"] = MapStrokeDashOffset,
|
||||||
|
[nameof(IBorderStroke.StrokeLineCap)] = MapStrokeLineCap,
|
||||||
|
[nameof(IBorderStroke.StrokeLineJoin)] = MapStrokeLineJoin,
|
||||||
|
[nameof(IBorderStroke.StrokeMiterLimit)] = MapStrokeMiterLimit,
|
||||||
|
["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
[nameof(IPadding.Padding)] = MapPadding,
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
|
["WidthRequest"] = MapWidthRequest,
|
||||||
|
["HeightRequest"] = MapHeightRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IBorderView, BorderHandler> CommandMapper =
|
public static CommandMapper<IBorderView, BorderHandler> CommandMapper =
|
||||||
@@ -46,24 +56,72 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
protected override void ConnectHandler(SkiaBorder platformView)
|
protected override void ConnectHandler(SkiaBorder platformView)
|
||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
|
if (VirtualView is View view)
|
||||||
|
{
|
||||||
|
platformView.MauiView = view;
|
||||||
|
}
|
||||||
|
platformView.Tapped += OnPlatformViewTapped;
|
||||||
|
|
||||||
|
// Explicitly map properties since they may be set before handler creation
|
||||||
|
if (VirtualView is VisualElement ve)
|
||||||
|
{
|
||||||
|
if (ve.BackgroundColor != null)
|
||||||
|
{
|
||||||
|
platformView.BackgroundColor = ve.BackgroundColor;
|
||||||
|
}
|
||||||
|
else if (ve.Background is SolidColorBrush brush && brush.Color != null)
|
||||||
|
{
|
||||||
|
platformView.BackgroundColor = brush.Color;
|
||||||
|
}
|
||||||
|
if (ve.WidthRequest >= 0)
|
||||||
|
{
|
||||||
|
platformView.WidthRequest = ve.WidthRequest;
|
||||||
|
}
|
||||||
|
if (ve.HeightRequest >= 0)
|
||||||
|
{
|
||||||
|
platformView.HeightRequest = ve.HeightRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaBorder platformView)
|
protected override void DisconnectHandler(SkiaBorder platformView)
|
||||||
{
|
{
|
||||||
|
platformView.Tapped -= OnPlatformViewTapped;
|
||||||
|
platformView.MauiView = null;
|
||||||
base.DisconnectHandler(platformView);
|
base.DisconnectHandler(platformView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPlatformViewTapped(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (VirtualView is View view)
|
||||||
|
{
|
||||||
|
GestureManager.ProcessTap(view, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapContent(BorderHandler handler, IBorderView border)
|
public static void MapContent(BorderHandler handler, IBorderView border)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||||
|
|
||||||
handler.PlatformView.ClearChildren();
|
handler.PlatformView.ClearChildren();
|
||||||
|
|
||||||
if (border.PresentedContent?.Handler?.PlatformView is SkiaView skiaContent)
|
var content = border.PresentedContent;
|
||||||
|
if (content != null)
|
||||||
{
|
{
|
||||||
|
// Create handler for content if it doesn't exist
|
||||||
|
if (content.Handler == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
|
||||||
|
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[BorderHandler] Adding content: {skiaContent.GetType().Name}");
|
||||||
handler.PlatformView.AddChild(skiaContent);
|
handler.PlatformView.AddChild(skiaContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapStroke(BorderHandler handler, IBorderView border)
|
public static void MapStroke(BorderHandler handler, IBorderView border)
|
||||||
{
|
{
|
||||||
@@ -71,14 +129,14 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
|
|
||||||
if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.Stroke = solidPaint.Color.ToSKColor();
|
handler.PlatformView.Stroke = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapStrokeThickness(BorderHandler handler, IBorderView border)
|
public static void MapStrokeThickness(BorderHandler handler, IBorderView border)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.StrokeThickness = (float)border.StrokeThickness;
|
handler.PlatformView.StrokeThickness = border.StrokeThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(BorderHandler handler, IBorderView border)
|
public static void MapBackground(BorderHandler handler, IBorderView border)
|
||||||
@@ -87,7 +145,23 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
|
|
||||||
if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.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;
|
||||||
|
Console.WriteLine($"[BorderHandler] MapBackgroundColor: {bgColor}");
|
||||||
|
if (bgColor != null)
|
||||||
|
{
|
||||||
|
handler.PlatformView.BackgroundColor = bgColor;
|
||||||
|
handler.PlatformView.Invalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +170,123 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
var padding = border.Padding;
|
var padding = border.Padding;
|
||||||
handler.PlatformView.PaddingLeft = (float)padding.Left;
|
handler.PlatformView.PaddingLeft = padding.Left;
|
||||||
handler.PlatformView.PaddingTop = (float)padding.Top;
|
handler.PlatformView.PaddingTop = padding.Top;
|
||||||
handler.PlatformView.PaddingRight = (float)padding.Right;
|
handler.PlatformView.PaddingRight = padding.Right;
|
||||||
handler.PlatformView.PaddingBottom = (float)padding.Bottom;
|
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,6 +1,7 @@
|
|||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@@ -20,6 +21,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
|||||||
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
|
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
[nameof(IPadding.Padding)] = MapPadding,
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -47,6 +49,32 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
|||||||
platformView.Clicked += OnClicked;
|
platformView.Clicked += OnClicked;
|
||||||
platformView.Pressed += OnPressed;
|
platformView.Pressed += OnPressed;
|
||||||
platformView.Released += OnReleased;
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ButtonHandler] VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaButton platformView)
|
protected override void DisconnectHandler(SkiaButton platformView)
|
||||||
@@ -67,13 +95,13 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
|||||||
|
|
||||||
var strokeColor = button.StrokeColor;
|
var strokeColor = button.StrokeColor;
|
||||||
if (strokeColor is not null)
|
if (strokeColor is not null)
|
||||||
handler.PlatformView.BorderColor = strokeColor.ToSKColor();
|
handler.PlatformView.BorderColor = strokeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapStrokeThickness(ButtonHandler handler, IButton button)
|
public static void MapStrokeThickness(ButtonHandler handler, IButton button)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
|
handler.PlatformView.BorderWidth = button.StrokeThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapCornerRadius(ButtonHandler handler, IButton button)
|
public static void MapCornerRadius(ButtonHandler handler, IButton button)
|
||||||
@@ -88,7 +116,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
|||||||
|
|
||||||
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
// Set BackgroundColor (MAUI Color type)
|
||||||
|
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,11 +126,18 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
var padding = button.Padding;
|
var padding = button.Padding;
|
||||||
handler.PlatformView.Padding = new SKRect(
|
handler.PlatformView.Padding = new Thickness(
|
||||||
(float)padding.Left,
|
padding.Left,
|
||||||
(float)padding.Top,
|
padding.Top,
|
||||||
(float)padding.Right,
|
padding.Right,
|
||||||
(float)padding.Bottom);
|
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 +160,33 @@ public partial class TextButtonHandler : ButtonHandler
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaButton platformView)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[TextButtonHandler] ConnectHandler DONE");
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapText(TextButtonHandler handler, ITextButton button)
|
public static void MapText(TextButtonHandler handler, ITextButton button)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
@@ -135,7 +198,7 @@ public partial class TextButtonHandler : ButtonHandler
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (button.TextColor is not null)
|
if (button.TextColor is not null)
|
||||||
handler.PlatformView.TextColor = button.TextColor.ToSKColor();
|
handler.PlatformView.TextColor = button.TextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapFont(TextButtonHandler handler, ITextButton button)
|
public static void MapFont(TextButtonHandler handler, ITextButton button)
|
||||||
@@ -144,18 +207,23 @@ public partial class TextButtonHandler : ButtonHandler
|
|||||||
|
|
||||||
var font = button.Font;
|
var font = button.Font;
|
||||||
if (font.Size > 0)
|
if (font.Size > 0)
|
||||||
handler.PlatformView.FontSize = (float)font.Size;
|
handler.PlatformView.FontSize = font.Size;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(font.Family))
|
if (!string.IsNullOrEmpty(font.Family))
|
||||||
handler.PlatformView.FontFamily = font.Family;
|
handler.PlatformView.FontFamily = font.Family;
|
||||||
|
|
||||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
// Convert Font weight/slant to FontAttributes
|
||||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
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)
|
public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
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.IsChecked)] = MapIsChecked,
|
||||||
[nameof(ICheckBox.Foreground)] = MapForeground,
|
[nameof(ICheckBox.Foreground)] = MapForeground,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
|
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||||
|
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
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)
|
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)
|
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,7 @@ using Microsoft.Maui.Handlers;
|
|||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -15,6 +16,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCollectionView>
|
public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCollectionView>
|
||||||
{
|
{
|
||||||
|
private bool _isUpdatingSelection;
|
||||||
|
|
||||||
public static IPropertyMapper<CollectionView, CollectionViewHandler> Mapper =
|
public static IPropertyMapper<CollectionView, CollectionViewHandler> Mapper =
|
||||||
new PropertyMapper<CollectionView, CollectionViewHandler>(ViewHandler.ViewMapper)
|
new PropertyMapper<CollectionView, CollectionViewHandler>(ViewHandler.ViewMapper)
|
||||||
{
|
{
|
||||||
@@ -36,6 +39,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout,
|
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout,
|
||||||
|
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(CollectionView.BackgroundColor)] = MapBackgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<CollectionView, CollectionViewHandler> CommandMapper =
|
public static CommandMapper<CollectionView, CollectionViewHandler> CommandMapper =
|
||||||
@@ -76,12 +80,20 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
|
|
||||||
private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e)
|
private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (VirtualView is null) return;
|
if (VirtualView is null || _isUpdatingSelection) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isUpdatingSelection = true;
|
||||||
|
|
||||||
// Update virtual view selection
|
// Update virtual view selection
|
||||||
if (VirtualView.SelectionMode == SelectionMode.Single)
|
if (VirtualView.SelectionMode == SelectionMode.Single)
|
||||||
{
|
{
|
||||||
VirtualView.SelectedItem = e.CurrentSelection.FirstOrDefault();
|
var newItem = e.CurrentSelection.FirstOrDefault();
|
||||||
|
if (!Equals(VirtualView.SelectedItem, newItem))
|
||||||
|
{
|
||||||
|
VirtualView.SelectedItem = newItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
|
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
|
||||||
{
|
{
|
||||||
@@ -93,6 +105,11 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isUpdatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnScrolled(object? sender, ItemsScrolledEventArgs e)
|
private void OnScrolled(object? sender, ItemsScrolledEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -107,7 +124,49 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
|
|
||||||
private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e)
|
private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e)
|
||||||
{
|
{
|
||||||
// Item tap is handled through selection
|
if (VirtualView is null || _isUpdatingSelection) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isUpdatingSelection = true;
|
||||||
|
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine($"[CollectionViewHandler] GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? "null"}, MauiView={skiaView?.MauiView?.GetType().Name ?? "null"}");
|
||||||
|
|
||||||
|
if (skiaView?.MauiView != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[CollectionViewHandler] Found MauiView: {skiaView.MauiView.GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}");
|
||||||
|
if (GestureManager.ProcessTap(skiaView.MauiView, 0, 0))
|
||||||
|
{
|
||||||
|
Console.WriteLine("[CollectionViewHandler] Gesture processed successfully");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle selection if gesture wasn't processed
|
||||||
|
if (VirtualView.SelectionMode == SelectionMode.Single)
|
||||||
|
{
|
||||||
|
VirtualView.SelectedItem = e.Item;
|
||||||
|
}
|
||||||
|
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
|
||||||
|
{
|
||||||
|
if (VirtualView.SelectedItems.Contains(e.Item))
|
||||||
|
{
|
||||||
|
VirtualView.SelectedItems.Remove(e.Item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VirtualView.SelectedItems.Add(e.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isUpdatingSelection = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView)
|
public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView)
|
||||||
@@ -118,7 +177,68 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
|
|
||||||
public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView)
|
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;
|
||||||
|
Console.WriteLine($"[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)
|
public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView)
|
||||||
@@ -146,13 +266,29 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
|
|
||||||
public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView)
|
public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler._isUpdatingSelection = true;
|
||||||
|
if (!Equals(handler.PlatformView.SelectedItem, collectionView.SelectedItem))
|
||||||
|
{
|
||||||
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
|
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
handler._isUpdatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView)
|
public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler._isUpdatingSelection = true;
|
||||||
|
|
||||||
// Sync selected items
|
// Sync selected items
|
||||||
var selectedItems = collectionView.SelectedItems;
|
var selectedItems = collectionView.SelectedItems;
|
||||||
@@ -161,6 +297,11 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
handler.PlatformView.SelectedItem = selectedItems.First();
|
handler.PlatformView.SelectedItem = selectedItems.First();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
handler._isUpdatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapSelectionMode(CollectionViewHandler handler, CollectionView collectionView)
|
public static void MapSelectionMode(CollectionViewHandler handler, CollectionView collectionView)
|
||||||
{
|
{
|
||||||
@@ -214,9 +355,23 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
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)
|
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 +389,32 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
|||||||
handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated);
|
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.Format)] = MapFormat,
|
||||||
[nameof(IDatePicker.TextColor)] = MapTextColor,
|
[nameof(IDatePicker.TextColor)] = MapTextColor,
|
||||||
[nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing,
|
[nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
|
[nameof(ITextStyle.Font)] = MapFont,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +50,17 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
|||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.DateSelected += OnDateSelected;
|
platformView.DateSelected += OnDateSelected;
|
||||||
|
|
||||||
|
// Apply dark theme colors if dark mode is active
|
||||||
|
var current = Application.Current;
|
||||||
|
if (current != null && (int)current.UserAppTheme == 2) // Dark theme
|
||||||
|
{
|
||||||
|
platformView.CalendarBackgroundColor = Color.FromRgb(30, 30, 30);
|
||||||
|
platformView.TextColor = Color.FromRgb(224, 224, 224);
|
||||||
|
platformView.BorderColor = Color.FromRgb(97, 97, 97);
|
||||||
|
platformView.DisabledDayColor = Color.FromRgb(97, 97, 97);
|
||||||
|
platformView.BackgroundColor = Color.FromRgb(45, 45, 45);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaDatePicker platformView)
|
protected override void DisconnectHandler(SkiaDatePicker platformView)
|
||||||
@@ -57,11 +69,11 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
|||||||
base.DisconnectHandler(platformView);
|
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;
|
if (VirtualView is null || PlatformView is null) return;
|
||||||
|
|
||||||
VirtualView.Date = PlatformView.Date;
|
VirtualView.Date = e.NewDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapDate(DatePickerHandler handler, IDatePicker datePicker)
|
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 (handler.PlatformView is null) return;
|
||||||
if (datePicker.TextColor is not null)
|
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)
|
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)
|
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)
|
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.Placeholder)] = MapPlaceholder,
|
||||||
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
|
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
|
||||||
[nameof(IEditor.TextColor)] = MapTextColor,
|
[nameof(IEditor.TextColor)] = MapTextColor,
|
||||||
|
[nameof(ITextStyle.Font)] = MapFont,
|
||||||
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
|
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
|
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
|
||||||
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
|
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
|
||||||
|
[nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
|
||||||
[nameof(IEditor.MaxLength)] = MapMaxLength,
|
[nameof(IEditor.MaxLength)] = MapMaxLength,
|
||||||
[nameof(IEditor.CursorPosition)] = MapCursorPosition,
|
[nameof(IEditor.CursorPosition)] = MapCursorPosition,
|
||||||
[nameof(IEditor.SelectionLength)] = MapSelectionLength,
|
[nameof(IEditor.SelectionLength)] = MapSelectionLength,
|
||||||
@@ -31,6 +33,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
|||||||
[nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
[nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||||
[nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
[nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IEditor, EditorHandler> CommandMapper =
|
public static CommandMapper<IEditor, EditorHandler> CommandMapper =
|
||||||
@@ -82,6 +85,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
|
|||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.Text = editor.Text ?? "";
|
handler.PlatformView.Text = editor.Text ?? "";
|
||||||
|
handler.PlatformView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapPlaceholder(EditorHandler handler, IEditor editor)
|
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 (handler.PlatformView is null) return;
|
||||||
if (editor.PlaceholderColor is not null)
|
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 (handler.PlatformView is null) return;
|
||||||
if (editor.TextColor is not null)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -28,9 +29,13 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
[nameof(IEntry.IsPassword)] = MapIsPassword,
|
[nameof(IEntry.IsPassword)] = MapIsPassword,
|
||||||
[nameof(IEntry.ReturnType)] = MapReturnType,
|
[nameof(IEntry.ReturnType)] = MapReturnType,
|
||||||
[nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility,
|
[nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility,
|
||||||
|
[nameof(IEntry.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
|
||||||
|
[nameof(IEntry.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
|
||||||
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
|
["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -85,7 +90,10 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (handler.PlatformView.Text != entry.Text)
|
if (handler.PlatformView.Text != entry.Text)
|
||||||
|
{
|
||||||
handler.PlatformView.Text = entry.Text ?? string.Empty;
|
handler.PlatformView.Text = entry.Text ?? string.Empty;
|
||||||
|
handler.PlatformView.Invalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapTextColor(EntryHandler handler, IEntry entry)
|
public static void MapTextColor(EntryHandler handler, IEntry entry)
|
||||||
@@ -93,7 +101,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (entry.TextColor is not null)
|
if (entry.TextColor is not null)
|
||||||
handler.PlatformView.TextColor = entry.TextColor.ToSKColor();
|
handler.PlatformView.TextColor = entry.TextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapFont(EntryHandler handler, IEntry entry)
|
public static void MapFont(EntryHandler handler, IEntry entry)
|
||||||
@@ -102,19 +110,24 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
|
|
||||||
var font = entry.Font;
|
var font = entry.Font;
|
||||||
if (font.Size > 0)
|
if (font.Size > 0)
|
||||||
handler.PlatformView.FontSize = (float)font.Size;
|
handler.PlatformView.FontSize = font.Size;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(font.Family))
|
if (!string.IsNullOrEmpty(font.Family))
|
||||||
handler.PlatformView.FontFamily = font.Family;
|
handler.PlatformView.FontFamily = font.Family;
|
||||||
|
|
||||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
// Convert Font weight/slant to FontAttributes
|
||||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
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)
|
public static void MapCharacterSpacing(EntryHandler handler, IEntry entry)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
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)
|
public static void MapPlaceholder(EntryHandler handler, IEntry entry)
|
||||||
@@ -128,7 +141,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (entry.PlaceholderColor is not null)
|
if (entry.PlaceholderColor is not null)
|
||||||
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor();
|
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
|
public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
|
||||||
@@ -174,16 +187,28 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
handler.PlatformView.ShowClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing;
|
handler.PlatformView.ShowClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.IsTextPredictionEnabled = entry.IsTextPredictionEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapIsSpellCheckEnabled(EntryHandler handler, IEntry entry)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.IsSpellCheckEnabled = entry.IsSpellCheckEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
|
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
|
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
|
||||||
{
|
{
|
||||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||||
_ => Platform.TextAlignment.Start
|
_ => TextAlignment.Start
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +218,10 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
|
|
||||||
handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch
|
handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch
|
||||||
{
|
{
|
||||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||||
_ => Platform.TextAlignment.Center
|
_ => TextAlignment.Center
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +231,29 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
|
|
||||||
if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.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.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -13,12 +15,17 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage>
|
public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage>
|
||||||
{
|
{
|
||||||
|
private bool _isUpdatingPresented;
|
||||||
|
|
||||||
public static IPropertyMapper<IFlyoutView, FlyoutPageHandler> Mapper = new PropertyMapper<IFlyoutView, FlyoutPageHandler>(ViewHandler.ViewMapper)
|
public static IPropertyMapper<IFlyoutView, FlyoutPageHandler> Mapper = new PropertyMapper<IFlyoutView, FlyoutPageHandler>(ViewHandler.ViewMapper)
|
||||||
{
|
{
|
||||||
|
[nameof(IFlyoutView.Flyout)] = MapFlyout,
|
||||||
|
[nameof(IFlyoutView.Detail)] = MapDetail,
|
||||||
[nameof(IFlyoutView.IsPresented)] = MapIsPresented,
|
[nameof(IFlyoutView.IsPresented)] = MapIsPresented,
|
||||||
[nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth,
|
[nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth,
|
||||||
[nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled,
|
[nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled,
|
||||||
[nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior,
|
[nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior,
|
||||||
|
[nameof(IView.Background)] = MapBackground,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IFlyoutView, FlyoutPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IFlyoutView, FlyoutPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -55,14 +62,83 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
|
|||||||
|
|
||||||
private void OnIsPresentedChanged(object? sender, EventArgs e)
|
private void OnIsPresentedChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
if (VirtualView is null || PlatformView is null || _isUpdatingPresented) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isUpdatingPresented = true;
|
||||||
// Sync back to the virtual view
|
// Sync back to the virtual view
|
||||||
|
if (VirtualView is FlyoutPage flyoutPage)
|
||||||
|
{
|
||||||
|
flyoutPage.IsPresented = PlatformView.IsPresented;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isUpdatingPresented = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapFlyout(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||||
|
|
||||||
|
var flyout = flyoutView.Flyout;
|
||||||
|
if (flyout == null)
|
||||||
|
{
|
||||||
|
handler.PlatformView.Flyout = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handler for flyout content
|
||||||
|
if (flyout.Handler == null)
|
||||||
|
{
|
||||||
|
flyout.Handler = flyout.ToViewHandler(handler.MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flyout.Handler?.PlatformView is SkiaView skiaFlyout)
|
||||||
|
{
|
||||||
|
handler.PlatformView.Flyout = skiaFlyout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapDetail(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||||
|
|
||||||
|
var detail = flyoutView.Detail;
|
||||||
|
if (detail == null)
|
||||||
|
{
|
||||||
|
handler.PlatformView.Detail = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handler for detail content
|
||||||
|
if (detail.Handler == null)
|
||||||
|
{
|
||||||
|
detail.Handler = detail.ToViewHandler(handler.MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.Handler?.PlatformView is SkiaView skiaDetail)
|
||||||
|
{
|
||||||
|
handler.PlatformView.Detail = skiaDetail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null || handler._isUpdatingPresented) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler._isUpdatingPresented = true;
|
||||||
handler.PlatformView.IsPresented = flyoutView.IsPresented;
|
handler.PlatformView.IsPresented = flyoutView.IsPresented;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
handler._isUpdatingPresented = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||||
{
|
{
|
||||||
@@ -88,4 +164,14 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
|
|||||||
_ => FlyoutLayoutBehavior.Default
|
_ => FlyoutLayoutBehavior.Default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapBackground(FlyoutPageHandler handler, IFlyoutView flyoutView)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (flyoutView is FlyoutPage flyoutPage && flyoutPage.Background is SolidColorBrush solidBrush)
|
||||||
|
{
|
||||||
|
handler.PlatformView.ScrimColor = solidBrush.Color.WithAlpha(100f / 255f);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
845
Handlers/GestureManager.cs
Normal file
845
Handlers/GestureManager.cs
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo? _sendTappedMethod;
|
||||||
|
private static MethodInfo? _sendPinchMethod;
|
||||||
|
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new Dictionary<View, (DateTime, int)>();
|
||||||
|
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new Dictionary<View, GestureTrackingState>();
|
||||||
|
|
||||||
|
private const double SwipeMinDistance = 50.0;
|
||||||
|
private const double SwipeMaxTime = 500.0;
|
||||||
|
private const double SwipeDirectionThreshold = 0.5;
|
||||||
|
private const double PanMinDistance = 10.0;
|
||||||
|
private const double PinchScrollScale = 0.1; // Scale factor per scroll unit
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var tapRecognizer = (item is TapGestureRecognizer) ? (TapGestureRecognizer)item : null;
|
||||||
|
if (tapRecognizer == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GestureManager] 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);
|
||||||
|
Console.WriteLine($"[GestureManager] First tap 1/{numberOfTapsRequired}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!((utcNow - tracking.lastTap).TotalMilliseconds < 300.0))
|
||||||
|
{
|
||||||
|
_tapTracking[view] = (utcNow, 1);
|
||||||
|
Console.WriteLine($"[GestureManager] Tap timeout, reset to 1/{numberOfTapsRequired}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int tapCount = tracking.tapCount + 1;
|
||||||
|
if (tapCount < numberOfTapsRequired)
|
||||||
|
{
|
||||||
|
_tapTracking[view] = (utcNow, tapCount);
|
||||||
|
Console.WriteLine($"[GestureManager] Tap {tapCount}/{numberOfTapsRequired}, waiting for more taps");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_tapTracking.Remove(view);
|
||||||
|
}
|
||||||
|
bool eventFired = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_sendTappedMethod == null)
|
||||||
|
{
|
||||||
|
_sendTappedMethod = typeof(TapGestureRecognizer).GetMethod("SendTapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
}
|
||||||
|
if (_sendTappedMethod != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] Found SendTapped method with {_sendTappedMethod.GetParameters().Length} params");
|
||||||
|
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
|
||||||
|
_sendTappedMethod.Invoke(tapRecognizer, new object[] { view, args });
|
||||||
|
Console.WriteLine("[GestureManager] SendTapped invoked successfully");
|
||||||
|
eventFired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] SendTapped failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
if (!eventFired)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var field = typeof(TapGestureRecognizer).GetField("Tapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||||
|
?? typeof(TapGestureRecognizer).GetField("_tapped", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
if (field != null && field.GetValue(tapRecognizer) is EventHandler<TappedEventArgs> handler)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Invoking Tapped event directly");
|
||||||
|
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
|
||||||
|
handler(tapRecognizer, args);
|
||||||
|
eventFired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Direct event invoke failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!eventFired)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] fieldNames = new string[] { "TappedEvent", "_TappedHandler", "<Tapped>k__BackingField" };
|
||||||
|
foreach (string fieldName in fieldNames)
|
||||||
|
{
|
||||||
|
var field = typeof(TapGestureRecognizer).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
if (field != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Found field: " + fieldName);
|
||||||
|
if (field.GetValue(tapRecognizer) is EventHandler<TappedEventArgs> handler)
|
||||||
|
{
|
||||||
|
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
|
||||||
|
handler(tapRecognizer, args);
|
||||||
|
Console.WriteLine("[GestureManager] Event fired via " + fieldName);
|
||||||
|
eventFired = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Backing field approach failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!eventFired)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Could not fire event, dumping type info...");
|
||||||
|
var methods = typeof(TapGestureRecognizer).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
foreach (var method in methods)
|
||||||
|
{
|
||||||
|
if (method.Name.Contains("Tap", StringComparison.OrdinalIgnoreCase) || method.Name.Contains("Send", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] Method: {method.Name}({string.Join(", ", from p in method.GetParameters() select p.ParameterType.Name)})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ICommand? command = tapRecognizer.Command;
|
||||||
|
if (command != null && command.CanExecute(tapRecognizer.CommandParameter))
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Executing 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) >= 10.0)
|
||||||
|
{
|
||||||
|
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 >= 50.0 && elapsed <= 500.0)
|
||||||
|
{
|
||||||
|
var direction = DetermineSwipeDirection(deltaX, deltaY);
|
||||||
|
if (direction != SwipeDirection.Right)
|
||||||
|
{
|
||||||
|
ProcessSwipeGesture(view, direction);
|
||||||
|
}
|
||||||
|
else if (Math.Abs(deltaX) > Math.Abs(deltaY) * 0.5)
|
||||||
|
{
|
||||||
|
ProcessSwipeGesture(view, (deltaX > 0.0) ? SwipeDirection.Right : SwipeDirection.Left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.IsPanning)
|
||||||
|
{
|
||||||
|
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)2);
|
||||||
|
}
|
||||||
|
else if (distance < 15.0 && elapsed < 500.0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] 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 * 0.5)
|
||||||
|
{
|
||||||
|
if (deltaX > 0.0)
|
||||||
|
{
|
||||||
|
return SwipeDirection.Right;
|
||||||
|
}
|
||||||
|
return SwipeDirection.Left;
|
||||||
|
}
|
||||||
|
if (absY > absX * 0.5)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var swipeRecognizer = (item is SwipeGestureRecognizer) ? (SwipeGestureRecognizer)item : null;
|
||||||
|
if (swipeRecognizer == null || !swipeRecognizer.Direction.HasFlag(direction))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GestureManager] Swipe detected: {direction}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(SwipeGestureRecognizer).GetMethod("SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(swipeRecognizer, new object[] { view, direction });
|
||||||
|
Console.WriteLine("[GestureManager] SendSwiped invoked successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] SendSwiped failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
ICommand? command = swipeRecognizer.Command;
|
||||||
|
if (command != null && command.CanExecute(swipeRecognizer.CommandParameter))
|
||||||
|
{
|
||||||
|
swipeRecognizer.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)
|
||||||
|
{
|
||||||
|
var panRecognizer = (item is PanGestureRecognizer) ? (PanGestureRecognizer)item : null;
|
||||||
|
if (panRecognizer == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GestureManager] Pan gesture: status={status}, totalX={totalX:F1}, totalY={totalY:F1}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(PanGestureRecognizer).GetMethod("SendPan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(panRecognizer, new object[]
|
||||||
|
{
|
||||||
|
view,
|
||||||
|
totalX,
|
||||||
|
totalY,
|
||||||
|
(int)status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] SendPan failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var pointerRecognizer = (item is PointerGestureRecognizer) ? (PointerGestureRecognizer)item : null;
|
||||||
|
if (pointerRecognizer == null)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var method = typeof(PointerGestureRecognizer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
var args = CreatePointerEventArgs(view, x, y);
|
||||||
|
method.Invoke(pointerRecognizer, new object[] { view, args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GestureManager] Pointer event failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var pinchRecognizer = item as PinchGestureRecognizer;
|
||||||
|
if (pinchRecognizer == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Pinch gesture: status={status}, scale={scale:F2}, origin=({originX:F0},{originY:F0})");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cache the method lookup
|
||||||
|
if (_sendPinchMethod == null)
|
||||||
|
{
|
||||||
|
_sendPinchMethod = typeof(PinchGestureRecognizer).GetMethod("SendPinch",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sendPinchMethod != null)
|
||||||
|
{
|
||||||
|
// SendPinch(IView sender, double scale, Point scaleOrigin, GestureStatus status)
|
||||||
|
var scaleOrigin = new Point(originX / view.Width, originY / view.Height);
|
||||||
|
_sendPinchMethod.Invoke(pinchRecognizer, new object[]
|
||||||
|
{
|
||||||
|
view,
|
||||||
|
scale,
|
||||||
|
scaleOrigin,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
Console.WriteLine("[GestureManager] SendPinch invoked successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendPinch failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var dragRecognizer = item as DragGestureRecognizer;
|
||||||
|
if (dragRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Starting drag from {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create DragStartingEventArgs and invoke SendDragStarting
|
||||||
|
var method = typeof(DragGestureRecognizer).GetMethod("SendDragStarting",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dragRecognizer, new object[] { view });
|
||||||
|
Console.WriteLine("[GestureManager] SendDragStarting invoked successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDragStarting failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var dropRecognizer = item as DropGestureRecognizer;
|
||||||
|
if (dropRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Drag enter on {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(DropGestureRecognizer).GetMethod("SendDragOver",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dropRecognizer, new object[] { view });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDragOver failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var dropRecognizer = item as DropGestureRecognizer;
|
||||||
|
if (dropRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Drop on {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(DropGestureRecognizer).GetMethod("SendDrop",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dropRecognizer, new object[] { view });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDrop failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public partial class GraphicsViewHandler : ViewHandler<IGraphicsView, SkiaGraphi
|
|||||||
|
|
||||||
if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.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;
|
||||||
|
}
|
||||||
|
Console.WriteLine("[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Dialog result: {result}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error showing dialog: {ex.Message}");
|
||||||
|
e.Callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNavigationStarted(object? sender, string uri)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine("[GtkWebViewHandler] Sent Navigating event to VirtualView");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigating: {ex.Message}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation started: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNavigationCompleted(object? sender, (string Url, bool Success) e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Sent Navigated, CanGoBack={canGoBack}, CanGoForward={canGoForward}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigated: {ex.Message}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation completed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RegisterWithHost(SKRect bounds)
|
||||||
|
{
|
||||||
|
if (_platformWebView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var hostService = GtkHostService.Instance;
|
||||||
|
if (hostService.HostWindow == null || hostService.WebViewManager == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GtkWebViewHandler] Warning: 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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Skipping invalid bounds: {bounds}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isRegisteredWithHost)
|
||||||
|
{
|
||||||
|
hostService.HostWindow.AddWebView(_platformWebView.Widget, x, y, width, height);
|
||||||
|
_isRegisteredWithHost = true;
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Registered WebView at ({x}, {y}) size {width}x{height}");
|
||||||
|
}
|
||||||
|
else if (bounds != _lastBounds)
|
||||||
|
{
|
||||||
|
hostService.HostWindow.MoveResizeWebView(_platformWebView.Widget, x, y, width, height);
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine("[GtkWebViewHandler] Unregistered WebView from host");
|
||||||
|
}
|
||||||
|
_isRegisteredWithHost = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapSource(GtkWebViewHandler handler, IWebView webView)
|
||||||
|
{
|
||||||
|
if (handler._platformWebView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var source = webView.Source;
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] MapGoBack called, CanGoBack={handler._platformWebView?.CanGoBack()}");
|
||||||
|
handler._platformWebView?.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapGoForward(GtkWebViewHandler handler, IWebView webView, object? args)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] MapGoForward called, CanGoForward={handler._platformWebView?.CanGoForward()}");
|
||||||
|
handler._platformWebView?.GoForward();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapReload(GtkWebViewHandler handler, IWebView webView, object? args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
546
Handlers/GtkWebViewPlatformView.cs
Normal file
546
Handlers/GtkWebViewPlatformView.cs
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("[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) ?? "";
|
||||||
|
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Error in OnScriptDialog: {ex.Message}");
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[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);
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Error in HandlePromptDialog: {ex.Message}");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Error applying dialog theme: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string uri = WebKitNative.GetUri(webView) ?? _currentUri ?? "";
|
||||||
|
switch ((WebKitNative.WebKitLoadEvent)loadEvent)
|
||||||
|
{
|
||||||
|
case WebKitNative.WebKitLoadEvent.Started:
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Load started: " + uri);
|
||||||
|
NavigationStarted?.Invoke(this, uri);
|
||||||
|
break;
|
||||||
|
case WebKitNative.WebKitLoadEvent.Finished:
|
||||||
|
_currentUri = uri;
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Load finished: " + uri);
|
||||||
|
NavigationCompleted?.Invoke(this, (uri, true));
|
||||||
|
break;
|
||||||
|
case WebKitNative.WebKitLoadEvent.Committed:
|
||||||
|
_currentUri = uri;
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Load committed: " + uri);
|
||||||
|
break;
|
||||||
|
case WebKitNative.WebKitLoadEvent.Redirected:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Error in OnLoadChanged: " + ex.Message);
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Stack trace: " + ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Navigate(string uri)
|
||||||
|
{
|
||||||
|
if (_widget != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
WebKitNative.LoadUri(_widget, uri);
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Navigate to: " + uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadHtml(string html, string? baseUri = null)
|
||||||
|
{
|
||||||
|
if (_widget != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
WebKitNative.LoadHtml(_widget, html, baseUri);
|
||||||
|
Console.WriteLine("[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(IButtonStroke.CornerRadius)] = MapCornerRadius,
|
||||||
[nameof(IPadding.Padding)] = MapPadding,
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
|
[nameof(IView.Width)] = MapWidth,
|
||||||
|
[nameof(IView.Height)] = MapHeight,
|
||||||
|
["VerticalOptions"] = MapVerticalOptions,
|
||||||
|
["HorizontalOptions"] = MapHorizontalOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -118,13 +123,13 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (imageButton.StrokeColor is not null)
|
if (imageButton.StrokeColor is not null)
|
||||||
handler.PlatformView.StrokeColor = imageButton.StrokeColor.ToSKColor();
|
handler.PlatformView.StrokeColor = imageButton.StrokeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.StrokeThickness = (float)imageButton.StrokeThickness;
|
handler.PlatformView.StrokeThickness = imageButton.StrokeThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton)
|
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)
|
public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.Padding = imageButton.Padding;
|
||||||
var padding = imageButton.Padding;
|
|
||||||
handler.PlatformView.PaddingLeft = (float)padding.Left;
|
|
||||||
handler.PlatformView.PaddingTop = (float)padding.Top;
|
|
||||||
handler.PlatformView.PaddingRight = (float)padding.Right;
|
|
||||||
handler.PlatformView.PaddingBottom = (float)padding.Bottom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
@@ -150,7 +150,59 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
|
|
||||||
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.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,6 +1,8 @@
|
|||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@@ -20,6 +22,10 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
|||||||
[nameof(IImage.IsOpaque)] = MapIsOpaque,
|
[nameof(IImage.IsOpaque)] = MapIsOpaque,
|
||||||
[nameof(IImageSourcePart.Source)] = MapSource,
|
[nameof(IImageSourcePart.Source)] = MapSource,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["Width"] = MapWidth,
|
||||||
|
["Height"] = MapHeight,
|
||||||
|
["HorizontalOptions"] = MapHorizontalOptions,
|
||||||
|
["VerticalOptions"] = MapVerticalOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -88,6 +94,19 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
|||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
// Extract width/height requests from Image control
|
||||||
|
if (image is Image img)
|
||||||
|
{
|
||||||
|
if (img.WidthRequest > 0)
|
||||||
|
{
|
||||||
|
handler.PlatformView.WidthRequest = img.WidthRequest;
|
||||||
|
}
|
||||||
|
if (img.HeightRequest > 0)
|
||||||
|
{
|
||||||
|
handler.PlatformView.HeightRequest = img.HeightRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler.SourceLoader.UpdateImageSourceAsync();
|
handler.SourceLoader.UpdateImageSourceAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +116,57 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
|||||||
|
|
||||||
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.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;
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
Console.WriteLine($"[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 +231,14 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
|||||||
await _handler.PlatformView!.LoadFromStreamAsync(stream);
|
await _handler.PlatformView!.LoadFromStreamAsync(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (source is FontImageSource fontSource)
|
||||||
|
{
|
||||||
|
var bitmap = RenderFontImageSource(fontSource, _handler.PlatformView!.WidthRequest, _handler.PlatformView.HeightRequest);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
_handler.PlatformView.LoadFromBitmap(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -176,5 +253,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)
|
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.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Window;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -23,8 +26,13 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
|||||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
|
[nameof(ILabel.TextDecorations)] = MapTextDecorations,
|
||||||
[nameof(ILabel.LineHeight)] = MapLineHeight,
|
[nameof(ILabel.LineHeight)] = MapLineHeight,
|
||||||
|
["LineBreakMode"] = MapLineBreakMode,
|
||||||
|
["MaxLines"] = MapMaxLines,
|
||||||
[nameof(IPadding.Padding)] = MapPadding,
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||||
|
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||||
|
["FormattedText"] = MapFormattedText,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -45,6 +53,45 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
|||||||
return new SkiaLabel();
|
return new SkiaLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaLabel platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
if (VirtualView is View view)
|
||||||
|
{
|
||||||
|
platformView.MauiView = view;
|
||||||
|
|
||||||
|
// Set hand cursor if the label has tap gesture recognizers
|
||||||
|
if (view.GestureRecognizers.OfType<TapGestureRecognizer>().Any())
|
||||||
|
{
|
||||||
|
platformView.CursorType = CursorType.Hand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly map LineBreakMode on connect - MAUI may not trigger property change for defaults
|
||||||
|
if (VirtualView is Microsoft.Maui.Controls.Label mauiLabel)
|
||||||
|
{
|
||||||
|
platformView.LineBreakMode = mauiLabel.LineBreakMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
platformView.Tapped += OnPlatformViewTapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisconnectHandler(SkiaLabel platformView)
|
||||||
|
{
|
||||||
|
platformView.Tapped -= OnPlatformViewTapped;
|
||||||
|
platformView.MauiView = null;
|
||||||
|
base.DisconnectHandler(platformView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlatformViewTapped(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (VirtualView is View view)
|
||||||
|
{
|
||||||
|
GestureManager.ProcessTap(view, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapText(LabelHandler handler, ILabel label)
|
public static void MapText(LabelHandler handler, ILabel label)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
@@ -56,7 +103,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (label.TextColor is not null)
|
if (label.TextColor is not null)
|
||||||
handler.PlatformView.TextColor = label.TextColor.ToSKColor();
|
handler.PlatformView.TextColor = label.TextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapFont(LabelHandler handler, ILabel label)
|
public static void MapFont(LabelHandler handler, ILabel label)
|
||||||
@@ -65,32 +112,37 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
|||||||
|
|
||||||
var font = label.Font;
|
var font = label.Font;
|
||||||
if (font.Size > 0)
|
if (font.Size > 0)
|
||||||
handler.PlatformView.FontSize = (float)font.Size;
|
handler.PlatformView.FontSize = font.Size;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(font.Family))
|
if (!string.IsNullOrEmpty(font.Family))
|
||||||
handler.PlatformView.FontFamily = font.Family;
|
handler.PlatformView.FontFamily = font.Family;
|
||||||
|
|
||||||
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
|
// Convert Font weight/slant to FontAttributes
|
||||||
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
|
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)
|
public static void MapCharacterSpacing(LabelHandler handler, ILabel label)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
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)
|
public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
// Map MAUI TextAlignment to our internal TextAlignment
|
// Map MAUI TextAlignment to our TextAlignment
|
||||||
handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch
|
handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch
|
||||||
{
|
{
|
||||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||||
_ => Platform.TextAlignment.Start
|
_ => TextAlignment.Start
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,25 +152,45 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
|||||||
|
|
||||||
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
|
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
|
||||||
{
|
{
|
||||||
Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start,
|
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
|
||||||
Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center,
|
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
|
||||||
Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End,
|
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
|
||||||
_ => Platform.TextAlignment.Center
|
_ => TextAlignment.Center
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapTextDecorations(LabelHandler handler, ILabel label)
|
public static void MapTextDecorations(LabelHandler handler, ILabel label)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.TextDecorations = label.TextDecorations;
|
||||||
handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0;
|
|
||||||
handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapLineHeight(LabelHandler handler, ILabel label)
|
public static void MapLineHeight(LabelHandler handler, ILabel label)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.LineHeight = (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)
|
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;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
var padding = label.Padding;
|
var padding = label.Padding;
|
||||||
handler.PlatformView.Padding = new SKRect(
|
handler.PlatformView.Padding = new Thickness(
|
||||||
(float)padding.Left,
|
padding.Left,
|
||||||
(float)padding.Top,
|
padding.Top,
|
||||||
(float)padding.Right,
|
padding.Right,
|
||||||
(float)padding.Bottom);
|
padding.Bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(LabelHandler handler, ILabel label)
|
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)
|
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,7 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform;
|
namespace Microsoft.Maui.Platform;
|
||||||
@@ -17,7 +18,9 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
public static IPropertyMapper<ILayout, LayoutHandler> Mapper = new PropertyMapper<ILayout, LayoutHandler>(ViewHandler.ViewMapper)
|
public static IPropertyMapper<ILayout, LayoutHandler> Mapper = new PropertyMapper<ILayout, LayoutHandler>(ViewHandler.ViewMapper)
|
||||||
{
|
{
|
||||||
[nameof(ILayout.Background)] = MapBackground,
|
[nameof(ILayout.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
||||||
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -53,22 +56,84 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
return new SkiaStackLayout();
|
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)
|
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;
|
var background = layout.Background;
|
||||||
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
|
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
|
handler.PlatformView.BackgroundColor = solidBrush.Color;
|
||||||
}
|
}
|
||||||
handler.PlatformView.Invalidate();
|
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)
|
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
|
||||||
{
|
{
|
||||||
handler.PlatformView.ClipToBounds = layout.ClipsToBounds;
|
handler.PlatformView.ClipToBounds = layout.ClipsToBounds;
|
||||||
handler.PlatformView.Invalidate();
|
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)
|
public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg)
|
||||||
{
|
{
|
||||||
if (arg is LayoutHandlerUpdate update)
|
if (arg is LayoutHandlerUpdate update)
|
||||||
@@ -194,9 +259,16 @@ public partial class GridHandler : LayoutHandler
|
|||||||
{
|
{
|
||||||
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
||||||
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
|
[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 +277,52 @@ public partial class GridHandler : LayoutHandler
|
|||||||
return new SkiaGrid();
|
return new SkiaGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
// Map definitions on connect
|
||||||
|
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
|
||||||
|
UpdateRowDefinitions(grid, gridLayout);
|
||||||
|
UpdateColumnDefinitions(grid, gridLayout);
|
||||||
|
|
||||||
|
// Add existing children (important for template-created views)
|
||||||
|
for (int i = 0; i < gridLayout.Count; i++)
|
||||||
|
{
|
||||||
|
var child = gridLayout[i];
|
||||||
|
if (child == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
|
||||||
|
|
||||||
|
// Create handler for child if it doesn't exist
|
||||||
|
if (child.Handler == null)
|
||||||
|
{
|
||||||
|
child.Handler = child.ToViewHandler(MauiContext);
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||||
|
{
|
||||||
|
// Get grid position from attached properties
|
||||||
|
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
|
||||||
|
if (child is Microsoft.Maui.Controls.View mauiView)
|
||||||
|
{
|
||||||
|
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
|
||||||
|
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
|
||||||
|
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
|
||||||
|
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Adding child[{i}] at row={row}, col={column}");
|
||||||
|
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GridHandler.ConnectHandler] Grid now has {grid.Children.Count} SkiaView children");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapColumnSpacing(GridHandler handler, IGridLayout layout)
|
public static void MapColumnSpacing(GridHandler handler, IGridLayout layout)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is SkiaGrid grid)
|
if (handler.PlatformView is SkiaGrid grid)
|
||||||
@@ -222,6 +340,79 @@ public partial class GridHandler : LayoutHandler
|
|||||||
grid.Invalidate();
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -17,6 +18,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
{
|
{
|
||||||
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IPadding.Padding)] = MapPadding,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ILayout, LayoutHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<ILayout, LayoutHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -42,6 +44,38 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
return new SkiaStackLayout();
|
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)
|
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView == null) return;
|
if (handler.PlatformView == null) return;
|
||||||
@@ -54,7 +88,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
|
|
||||||
if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +136,18 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
|||||||
// Force re-layout
|
// Force re-layout
|
||||||
handler.PlatformView?.InvalidateMeasure();
|
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>
|
/// <summary>
|
||||||
@@ -138,6 +184,29 @@ public partial class StackLayoutHandler : LayoutHandler
|
|||||||
return new SkiaStackLayout();
|
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)
|
public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is SkiaStackLayout stackLayout)
|
if (handler.PlatformView is SkiaStackLayout stackLayout)
|
||||||
@@ -156,6 +225,8 @@ public partial class GridHandler : LayoutHandler
|
|||||||
{
|
{
|
||||||
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
|
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
|
||||||
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
|
||||||
|
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
|
||||||
|
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public GridHandler() : base(Mapper)
|
public GridHandler() : base(Mapper)
|
||||||
@@ -167,6 +238,76 @@ public partial class GridHandler : LayoutHandler
|
|||||||
return new SkiaGrid();
|
return new SkiaGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Don't call base - we handle children specially for Grid
|
||||||
|
if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GridHandler] ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols");
|
||||||
|
|
||||||
|
// Explicitly map BackgroundColor since it may be set before handler creation
|
||||||
|
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||||
|
{
|
||||||
|
platformView.BackgroundColor = ve.BackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly map Padding since it may be set before handler creation
|
||||||
|
if (VirtualView is IPadding paddable)
|
||||||
|
{
|
||||||
|
var padding = paddable.Padding;
|
||||||
|
platformView.Padding = padding;
|
||||||
|
Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map row/column definitions first
|
||||||
|
MapRowDefinitions(this, gridLayout);
|
||||||
|
MapColumnDefinitions(this, gridLayout);
|
||||||
|
|
||||||
|
// Add each child with its row/column position
|
||||||
|
for (int i = 0; i < gridLayout.Count; i++)
|
||||||
|
{
|
||||||
|
var child = gridLayout[i];
|
||||||
|
if (child == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}");
|
||||||
|
|
||||||
|
// Create handler for child if it doesn't exist
|
||||||
|
if (child.Handler == null)
|
||||||
|
{
|
||||||
|
child.Handler = child.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
|
||||||
|
|
||||||
|
// Add child's platform view to our grid
|
||||||
|
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||||
|
{
|
||||||
|
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
|
||||||
|
Console.WriteLine($"[GridHandler] Added child {i} to grid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[GridHandler] ConnectHandler complete");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapRowSpacing(GridHandler handler, IGridLayout layout)
|
public static void MapRowSpacing(GridHandler handler, IGridLayout layout)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is SkiaGrid grid)
|
if (handler.PlatformView is SkiaGrid grid)
|
||||||
@@ -182,4 +323,38 @@ public partial class GridHandler : LayoutHandler
|
|||||||
grid.ColumnSpacing = (float)layout.ColumnSpacing;
|
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,15 @@
|
|||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
using Svg.Skia;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
@@ -50,10 +54,15 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
platformView.Popped += OnPopped;
|
platformView.Popped += OnPopped;
|
||||||
platformView.PoppedToRoot += OnPoppedToRoot;
|
platformView.PoppedToRoot += OnPoppedToRoot;
|
||||||
|
|
||||||
// Set initial root page if exists
|
// Subscribe to navigation events from virtual view
|
||||||
if (VirtualView.CurrentPage != null)
|
if (VirtualView != null)
|
||||||
{
|
{
|
||||||
SetupInitialPage();
|
VirtualView.Pushed += OnVirtualViewPushed;
|
||||||
|
VirtualView.Popped += OnVirtualViewPopped;
|
||||||
|
VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot;
|
||||||
|
|
||||||
|
// Set up initial navigation stack
|
||||||
|
SetupNavigationStack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,16 +71,271 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
platformView.Pushed -= OnPushed;
|
platformView.Pushed -= OnPushed;
|
||||||
platformView.Popped -= OnPopped;
|
platformView.Popped -= OnPopped;
|
||||||
platformView.PoppedToRoot -= OnPoppedToRoot;
|
platformView.PoppedToRoot -= OnPoppedToRoot;
|
||||||
|
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
VirtualView.Pushed -= OnVirtualViewPushed;
|
||||||
|
VirtualView.Popped -= OnVirtualViewPopped;
|
||||||
|
VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot;
|
||||||
|
}
|
||||||
|
|
||||||
base.DisconnectHandler(platformView);
|
base.DisconnectHandler(platformView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupInitialPage()
|
private void SetupNavigationStack()
|
||||||
{
|
{
|
||||||
var currentPage = VirtualView.CurrentPage;
|
if (VirtualView == null || PlatformView == null || MauiContext == null) return;
|
||||||
if (currentPage?.Handler?.PlatformView is SkiaPage skiaPage)
|
|
||||||
|
// Get all pages in the navigation stack
|
||||||
|
var pages = VirtualView.Navigation.NavigationStack.ToList();
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Setting up {pages.Count} pages");
|
||||||
|
|
||||||
|
// If no pages in stack, check CurrentPage
|
||||||
|
if (pages.Count == 0 && VirtualView.CurrentPage != null)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
|
||||||
|
pages.Add(VirtualView.CurrentPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var page in pages)
|
||||||
|
{
|
||||||
|
// Ensure the page has a handler
|
||||||
|
if (page.Handler == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
|
||||||
|
page.Handler = page.ToViewHandler(MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
|
||||||
|
|
||||||
|
if (page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||||
|
{
|
||||||
|
// Set navigation bar properties
|
||||||
|
skiaPage.ShowNavigationBar = true;
|
||||||
|
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
|
||||||
|
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||||
|
skiaPage.Title = page.Title ?? "";
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||||
|
|
||||||
|
// If content is null, try to get it from ContentPage
|
||||||
|
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
|
||||||
|
if (contentPage.Content.Handler == null)
|
||||||
|
{
|
||||||
|
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
|
||||||
|
}
|
||||||
|
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
|
||||||
|
{
|
||||||
|
skiaPage.Content = skiaContent;
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map toolbar items
|
||||||
|
MapToolbarItems(skiaPage, page);
|
||||||
|
|
||||||
|
if (PlatformView.StackDepth == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Setting root page: {page.Title}");
|
||||||
PlatformView.SetRootPage(skiaPage);
|
PlatformView.SetRootPage(skiaPage);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Pushing page: {page.Title}");
|
||||||
|
PlatformView.Push(skiaPage, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Failed to get SkiaPage for: {page.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<Page, (SkiaPage, INotifyCollectionChanged)> _toolbarSubscriptions = new();
|
||||||
|
|
||||||
|
private void MapToolbarItems(SkiaPage skiaPage, Page page)
|
||||||
|
{
|
||||||
|
if (skiaPage is SkiaContentPage contentPage)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
|
||||||
|
|
||||||
|
contentPage.ToolbarItems.Clear();
|
||||||
|
foreach (var item in page.ToolbarItems)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', 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(() =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
|
||||||
|
// Use IMenuItemController to send the click
|
||||||
|
if (toolbarItem is IMenuItemController menuController)
|
||||||
|
{
|
||||||
|
menuController.Activate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: invoke Command if set
|
||||||
|
toolbarItem.Command?.Execute(toolbarItem.CommandParameter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Subscribing to ToolbarItems changes for '{page.Title}'");
|
||||||
|
notifyCollection.CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}");
|
||||||
|
MapToolbarItems(skiaPage, page);
|
||||||
|
skiaPage.Invalidate();
|
||||||
|
};
|
||||||
|
_toolbarSubscriptions[page] = (skiaPage, notifyCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SKBitmap? LoadToolbarIcon(string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string baseDirectory = AppContext.BaseDirectory;
|
||||||
|
string pngPath = Path.Combine(baseDirectory, fileName);
|
||||||
|
string svgPath = Path.Combine(baseDirectory, Path.ChangeExtension(fileName, ".svg"));
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] LoadToolbarIcon: Looking for {fileName}");
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Trying PNG: {pngPath} (exists: {File.Exists(pngPath)})");
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Loaded SVG icon: {svgPath}");
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PNG
|
||||||
|
if (File.Exists(pngPath))
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(pngPath);
|
||||||
|
var result = SKBitmap.Decode(stream);
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Loaded PNG icon: {pngPath}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Icon not found: {fileName}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Error loading icon {fileName}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}");
|
||||||
|
if (e.Page == null || PlatformView == null || MauiContext == null) return;
|
||||||
|
|
||||||
|
// Ensure the page has a handler
|
||||||
|
if (e.Page.Handler == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
|
||||||
|
e.Page.Handler = e.Page.ToViewHandler(MauiContext);
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||||
|
skiaPage.ShowNavigationBar = true;
|
||||||
|
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
|
||||||
|
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||||
|
skiaPage.Title = e.Page.Title ?? "";
|
||||||
|
|
||||||
|
// Handle content if null
|
||||||
|
if (skiaPage.Content == null && e.Page is ContentPage contentPage && contentPage.Content != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items");
|
||||||
|
MapToolbarItems(skiaPage, e.Page);
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Pushing page to platform");
|
||||||
|
PlatformView.Push(skiaPage, false);
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Push complete, thread={Environment.CurrentManagedThreadId}");
|
||||||
|
}
|
||||||
|
Console.WriteLine("[NavigationPageHandler] OnVirtualViewPushed returning");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}");
|
||||||
|
// Pop on the platform side to sync with MAUI navigation
|
||||||
|
PlatformView?.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot");
|
||||||
|
PlatformView?.PopToRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPushed(object? sender, NavigationEventArgs e)
|
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)
|
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)
|
private void OnPoppedToRoot(object? sender, NavigationEventArgs e)
|
||||||
@@ -95,7 +364,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
|
|
||||||
if (navigationPage.BarBackgroundColor is not null)
|
if (navigationPage.BarBackgroundColor is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor();
|
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +374,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
|
|
||||||
if (navigationPage.BarBackground is SolidColorBrush solidBrush)
|
if (navigationPage.BarBackground is SolidColorBrush solidBrush)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BarBackgroundColor = solidBrush.Color.ToSKColor();
|
handler.PlatformView.BarBackgroundColor = solidBrush.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +384,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
|
|
||||||
if (navigationPage.BarTextColor is not null)
|
if (navigationPage.BarTextColor is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor();
|
handler.PlatformView.BarTextColor = navigationPage.BarTextColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,20 +394,35 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
|||||||
|
|
||||||
if (navigationPage.Background is SolidColorBrush solidBrush)
|
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)
|
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;
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {request.NavigationStack.Count} pages");
|
||||||
|
|
||||||
// Handle navigation request
|
// 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)
|
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)
|
if (handler.PlatformView.StackDepth == 0)
|
||||||
{
|
{
|
||||||
handler.PlatformView.SetRootPage(skiaPage);
|
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,7 @@ using Microsoft.Maui.Handlers;
|
|||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -19,8 +20,11 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
|||||||
{
|
{
|
||||||
[nameof(Page.Title)] = MapTitle,
|
[nameof(Page.Title)] = MapTitle,
|
||||||
[nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource,
|
[nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource,
|
||||||
|
[nameof(Page.IconImageSource)] = MapIconImageSource,
|
||||||
[nameof(Page.Padding)] = MapPadding,
|
[nameof(Page.Padding)] = MapPadding,
|
||||||
|
[nameof(Page.IsBusy)] = MapIsBusy,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(VisualElement.BackgroundColor)] = MapBackgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<Page, PageHandler> CommandMapper =
|
public static CommandMapper<Page, PageHandler> CommandMapper =
|
||||||
@@ -45,6 +49,10 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
|||||||
protected override void ConnectHandler(SkiaPage platformView)
|
protected override void ConnectHandler(SkiaPage platformView)
|
||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
// Set MauiPage reference for theme refresh support
|
||||||
|
platformView.MauiPage = VirtualView;
|
||||||
|
|
||||||
platformView.Appearing += OnAppearing;
|
platformView.Appearing += OnAppearing;
|
||||||
platformView.Disappearing += OnDisappearing;
|
platformView.Disappearing += OnDisappearing;
|
||||||
}
|
}
|
||||||
@@ -53,11 +61,13 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
|||||||
{
|
{
|
||||||
platformView.Appearing -= OnAppearing;
|
platformView.Appearing -= OnAppearing;
|
||||||
platformView.Disappearing -= OnDisappearing;
|
platformView.Disappearing -= OnDisappearing;
|
||||||
|
platformView.MauiPage = null;
|
||||||
base.DisconnectHandler(platformView);
|
base.DisconnectHandler(platformView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAppearing(object? sender, EventArgs e)
|
private void OnAppearing(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}");
|
||||||
(VirtualView as IPageController)?.SendAppearing();
|
(VirtualView as IPageController)?.SendAppearing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +106,34 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
|||||||
|
|
||||||
if (page.Background is SolidColorBrush solidBrush)
|
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;
|
||||||
|
Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapIconImageSource(PageHandler handler, Page page)
|
||||||
|
{
|
||||||
|
// Icon is typically used by navigation containers (Shell, TabbedPage)
|
||||||
|
// Store for later use but don't render directly on the page
|
||||||
|
handler.PlatformView?.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapIsBusy(PageHandler handler, Page page)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.IsBusy = page.IsBusy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -110,6 +145,7 @@ public partial class ContentPageHandler : PageHandler
|
|||||||
new PropertyMapper<ContentPage, ContentPageHandler>(PageHandler.Mapper)
|
new PropertyMapper<ContentPage, ContentPageHandler>(PageHandler.Mapper)
|
||||||
{
|
{
|
||||||
[nameof(ContentPage.Content)] = MapContent,
|
[nameof(ContentPage.Content)] = MapContent,
|
||||||
|
[nameof(ContentPage.ToolbarItems)] = MapToolbarItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static new CommandMapper<ContentPage, ContentPageHandler> CommandMapper =
|
public static new CommandMapper<ContentPage, ContentPageHandler> CommandMapper =
|
||||||
@@ -131,24 +167,80 @@ public partial class ContentPageHandler : PageHandler
|
|||||||
return new SkiaContentPage();
|
return new SkiaContentPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaPage platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
// Sync toolbar items initially
|
||||||
|
if (VirtualView is ContentPage contentPage && platformView is SkiaContentPage skiaContentPage)
|
||||||
|
{
|
||||||
|
SyncToolbarItems(skiaContentPage, contentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapContent(ContentPageHandler handler, ContentPage page)
|
public static void MapContent(ContentPageHandler handler, ContentPage page)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null || handler.MauiContext is null) return;
|
||||||
|
|
||||||
// Get the platform view for the content
|
// Get the platform view for the content
|
||||||
var content = page.Content;
|
var content = page.Content;
|
||||||
if (content != null)
|
if (content != null)
|
||||||
{
|
{
|
||||||
// The content's handler should provide the platform view
|
// Create handler for content if it doesn't exist
|
||||||
var contentHandler = content.Handler;
|
if (content.Handler == null)
|
||||||
if (contentHandler?.PlatformView is SkiaView skiaContent)
|
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}");
|
||||||
handler.PlatformView.Content = skiaContent;
|
handler.PlatformView.Content = skiaContent;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
handler.PlatformView.Content = null;
|
handler.PlatformView.Content = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapToolbarItems(ContentPageHandler handler, ContentPage page)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is not SkiaContentPage skiaContentPage) return;
|
||||||
|
SyncToolbarItems(skiaContentPage, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SyncToolbarItems(SkiaContentPage platformView, ContentPage page)
|
||||||
|
{
|
||||||
|
platformView.ToolbarItems.Clear();
|
||||||
|
|
||||||
|
foreach (var item in page.ToolbarItems)
|
||||||
|
{
|
||||||
|
var skiaItem = new SkiaToolbarItem
|
||||||
|
{
|
||||||
|
Text = item.Text ?? "",
|
||||||
|
Command = item.Command,
|
||||||
|
Order = item.Order == ToolbarItemOrder.Primary
|
||||||
|
? SkiaToolbarItemOrder.Primary
|
||||||
|
: SkiaToolbarItemOrder.Secondary
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load icon if present
|
||||||
|
if (item.IconImageSource is FileImageSource fileSource)
|
||||||
|
{
|
||||||
|
// Icon loading would be async - simplified for now
|
||||||
|
Console.WriteLine($"[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.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
using SkiaSharp;
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for Picker on Linux using Skia rendering.
|
/// Handler for Picker on Linux using Skia rendering.
|
||||||
|
/// Maps IPicker interface to SkiaPicker platform view.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||||
{
|
{
|
||||||
@@ -21,10 +22,13 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
|||||||
[nameof(IPicker.TitleColor)] = MapTitleColor,
|
[nameof(IPicker.TitleColor)] = MapTitleColor,
|
||||||
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
|
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
|
||||||
[nameof(IPicker.TextColor)] = MapTextColor,
|
[nameof(IPicker.TextColor)] = MapTextColor,
|
||||||
|
[nameof(ITextStyle.Font)] = MapFont,
|
||||||
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
|
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||||
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
|
[nameof(Picker.ItemsSource)] = MapItemsSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IPicker, PickerHandler> CommandMapper =
|
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)
|
public PickerHandler() : base(Mapper, CommandMapper)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -51,21 +57,52 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
|||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
|
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();
|
ReloadItems();
|
||||||
|
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
MapTitle(this, VirtualView);
|
||||||
|
MapTitleColor(this, VirtualView);
|
||||||
|
MapTextColor(this, VirtualView);
|
||||||
|
MapSelectedIndex(this, VirtualView);
|
||||||
|
MapIsEnabled(this, VirtualView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaPicker platformView)
|
protected override void DisconnectHandler(SkiaPicker platformView)
|
||||||
{
|
{
|
||||||
platformView.SelectedIndexChanged -= OnSelectedIndexChanged;
|
platformView.SelectedIndexChanged -= OnSelectedIndexChanged;
|
||||||
|
|
||||||
|
if (_itemsCollection != null)
|
||||||
|
{
|
||||||
|
_itemsCollection.CollectionChanged -= OnItemsCollectionChanged;
|
||||||
|
_itemsCollection = null;
|
||||||
|
}
|
||||||
|
|
||||||
base.DisconnectHandler(platformView);
|
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;
|
if (VirtualView is null || PlatformView is null) return;
|
||||||
|
|
||||||
VirtualView.SelectedIndex = PlatformView.SelectedIndex;
|
if (VirtualView.SelectedIndex != e.NewIndex)
|
||||||
|
{
|
||||||
|
VirtualView.SelectedIndex = e.NewIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadItems()
|
private void ReloadItems()
|
||||||
@@ -87,38 +124,68 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
if (picker.TitleColor is not null)
|
if (picker.TitleColor is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor();
|
handler.PlatformView.TitleColor = picker.TitleColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapSelectedIndex(PickerHandler handler, IPicker picker)
|
public static void MapSelectedIndex(PickerHandler handler, IPicker picker)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (handler.PlatformView.SelectedIndex != picker.SelectedIndex)
|
||||||
|
{
|
||||||
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
|
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapTextColor(PickerHandler handler, IPicker picker)
|
public static void MapTextColor(PickerHandler handler, IPicker picker)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
if (picker.TextColor is not null)
|
if (picker.TextColor is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.TextColor = picker.TextColor.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)
|
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)
|
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)
|
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)
|
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)
|
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.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using Microsoft.Maui.Platform;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
@@ -18,7 +20,12 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
{
|
{
|
||||||
[nameof(IProgress.Progress)] = MapProgress,
|
[nameof(IProgress.Progress)] = MapProgress,
|
||||||
[nameof(IProgress.ProgressColor)] = MapProgressColor,
|
[nameof(IProgress.ProgressColor)] = MapProgressColor,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
|
[nameof(IView.Height)] = MapHeight,
|
||||||
|
[nameof(IView.Width)] = MapWidth,
|
||||||
|
["VerticalOptions"] = MapVerticalOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -39,6 +46,48 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
return new SkiaProgressBar();
|
return new SkiaProgressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ConnectHandler(SkiaProgressBar platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
if (VirtualView is BindableObject bindable)
|
||||||
|
{
|
||||||
|
bindable.PropertyChanged += OnVirtualViewPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VirtualView is VisualElement visualElement)
|
||||||
|
{
|
||||||
|
platformView.IsVisible = visualElement.IsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync properties
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
MapProgress(this, VirtualView);
|
||||||
|
MapProgressColor(this, VirtualView);
|
||||||
|
MapIsEnabled(this, VirtualView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisconnectHandler(SkiaProgressBar platformView)
|
||||||
|
{
|
||||||
|
if (VirtualView is BindableObject bindable)
|
||||||
|
{
|
||||||
|
bindable.PropertyChanged -= OnVirtualViewPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.DisconnectHandler(platformView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVirtualViewPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (VirtualView is VisualElement visualElement && e.PropertyName == nameof(VisualElement.IsVisible))
|
||||||
|
{
|
||||||
|
PlatformView.IsVisible = visualElement.IsVisible;
|
||||||
|
PlatformView.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
|
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
@@ -50,7 +99,18 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (progress.ProgressColor is not null)
|
if (progress.ProgressColor is not null)
|
||||||
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
|
{
|
||||||
|
handler.PlatformView.ProgressColor = progress.ProgressColor;
|
||||||
|
}
|
||||||
|
handler.PlatformView.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
handler.PlatformView.IsEnabled = progress.IsEnabled;
|
||||||
|
handler.PlatformView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
|
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
|
||||||
@@ -59,7 +119,49 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
|
|
||||||
if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.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)
|
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)
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
Handlers/ScrollViewHandler.cs
Normal file
110
Handlers/ScrollViewHandler.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for ScrollView on Linux using SkiaScrollView.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView>
|
||||||
|
{
|
||||||
|
public static IPropertyMapper<IScrollView, ScrollViewHandler> Mapper =
|
||||||
|
new PropertyMapper<IScrollView, ScrollViewHandler>(ViewMapper)
|
||||||
|
{
|
||||||
|
[nameof(IScrollView.Content)] = MapContent,
|
||||||
|
[nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility,
|
||||||
|
[nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility,
|
||||||
|
[nameof(IScrollView.Orientation)] = MapOrientation,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static CommandMapper<IScrollView, ScrollViewHandler> CommandMapper =
|
||||||
|
new(ViewCommandMapper)
|
||||||
|
{
|
||||||
|
[nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo
|
||||||
|
};
|
||||||
|
|
||||||
|
public ScrollViewHandler() : base(Mapper, CommandMapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScrollViewHandler(IPropertyMapper? mapper)
|
||||||
|
: base(mapper ?? Mapper, CommandMapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SkiaScrollView CreatePlatformView()
|
||||||
|
{
|
||||||
|
return new SkiaScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapContent(ScrollViewHandler handler, IScrollView scrollView)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView == null || handler.MauiContext == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var content = scrollView.PresentedContent;
|
||||||
|
if (content != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}");
|
||||||
|
|
||||||
|
// Create handler for content if it doesn't exist
|
||||||
|
if (content.Handler == null)
|
||||||
|
{
|
||||||
|
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}");
|
||||||
|
handler.PlatformView.Content = skiaContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handler.PlatformView.Content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
|
||||||
|
{
|
||||||
|
handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch
|
||||||
|
{
|
||||||
|
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
|
||||||
|
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
|
||||||
|
_ => ScrollBarVisibility.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
|
||||||
|
{
|
||||||
|
handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch
|
||||||
|
{
|
||||||
|
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
|
||||||
|
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
|
||||||
|
_ => ScrollBarVisibility.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView)
|
||||||
|
{
|
||||||
|
handler.PlatformView.Orientation = scrollView.Orientation switch
|
||||||
|
{
|
||||||
|
Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal,
|
||||||
|
Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both,
|
||||||
|
Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither,
|
||||||
|
_ => ScrollOrientation.Vertical
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args)
|
||||||
|
{
|
||||||
|
if (args is ScrollToRequest request)
|
||||||
|
{
|
||||||
|
// Instant means no animation, so we pass !Instant for animated parameter
|
||||||
|
handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(ITextInput.Text)] = MapText,
|
||||||
[nameof(ITextStyle.TextColor)] = MapTextColor,
|
[nameof(ITextStyle.TextColor)] = MapTextColor,
|
||||||
[nameof(ITextStyle.Font)] = MapFont,
|
[nameof(ITextStyle.Font)] = MapFont,
|
||||||
|
[nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
[nameof(IPlaceholder.Placeholder)] = MapPlaceholder,
|
[nameof(IPlaceholder.Placeholder)] = MapPlaceholder,
|
||||||
[nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor,
|
[nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor,
|
||||||
[nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor,
|
[nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor,
|
||||||
|
[nameof(ISearchBar.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (searchBar.TextColor is not null)
|
if (searchBar.TextColor is not null)
|
||||||
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
|
handler.PlatformView.TextColor = searchBar.TextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
|
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
|
||||||
@@ -93,10 +95,28 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
|||||||
|
|
||||||
var font = searchBar.Font;
|
var font = searchBar.Font;
|
||||||
if (font.Size > 0)
|
if (font.Size > 0)
|
||||||
handler.PlatformView.FontSize = (float)font.Size;
|
handler.PlatformView.FontSize = font.Size;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(font.Family))
|
if (!string.IsNullOrEmpty(font.Family))
|
||||||
handler.PlatformView.FontFamily = font.Family;
|
handler.PlatformView.FontFamily = font.Family;
|
||||||
|
|
||||||
|
// Map FontAttributes from the Font weight
|
||||||
|
var attrs = FontAttributes.None;
|
||||||
|
if (font.Weight >= FontWeight.Bold)
|
||||||
|
attrs |= FontAttributes.Bold;
|
||||||
|
handler.PlatformView.FontAttributes = attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapCharacterSpacing(SearchBarHandler handler, ISearchBar searchBar)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.CharacterSpacing = searchBar.CharacterSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapHorizontalTextAlignment(SearchBarHandler handler, ISearchBar searchBar)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.HorizontalTextAlignment = searchBar.HorizontalTextAlignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
|
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
|
||||||
@@ -110,7 +130,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (searchBar.PlaceholderColor is not null)
|
if (searchBar.PlaceholderColor is not null)
|
||||||
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
|
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)
|
public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)
|
||||||
@@ -119,7 +139,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
|
|||||||
|
|
||||||
// CancelButtonColor maps to ClearButtonColor
|
// CancelButtonColor maps to ClearButtonColor
|
||||||
if (searchBar.CancelButtonColor is not null)
|
if (searchBar.CancelButtonColor is not null)
|
||||||
handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor.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)
|
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,10 @@
|
|||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -10,14 +12,29 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for Shell on Linux using Skia rendering.
|
/// Handler for Shell on Linux using Skia rendering.
|
||||||
/// </summary>
|
/// </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)
|
public ShellHandler() : base(Mapper, CommandMapper)
|
||||||
@@ -31,30 +48,372 @@ public partial class ShellHandler : ViewHandler<IView, SkiaShell>
|
|||||||
|
|
||||||
protected override SkiaShell CreatePlatformView()
|
protected override SkiaShell CreatePlatformView()
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("[ShellHandler] CreatePlatformView - creating SkiaShell");
|
||||||
return new SkiaShell();
|
return new SkiaShell();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ConnectHandler(SkiaShell platformView)
|
protected override void ConnectHandler(SkiaShell platformView)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("[ShellHandler] ConnectHandler - connecting to SkiaShell");
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
||||||
platformView.Navigated += OnNavigated;
|
platformView.Navigated += OnNavigated;
|
||||||
|
|
||||||
|
// Store reference to MAUI Shell for callbacks
|
||||||
|
platformView.MauiShell = VirtualView;
|
||||||
|
|
||||||
|
// Set up content renderer
|
||||||
|
platformView.ContentRenderer = RenderShellContent;
|
||||||
|
platformView.ColorRefresher = RefreshShellColors;
|
||||||
|
|
||||||
|
// Subscribe to Shell navigation events
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
VirtualView.Navigating += OnShellNavigating;
|
||||||
|
VirtualView.Navigated += OnShellNavigated;
|
||||||
|
|
||||||
|
// Initial sync of shell items
|
||||||
|
SyncShellItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaShell platformView)
|
protected override void DisconnectHandler(SkiaShell platformView)
|
||||||
{
|
{
|
||||||
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
|
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
|
||||||
platformView.Navigated -= OnNavigated;
|
platformView.Navigated -= OnNavigated;
|
||||||
|
platformView.MauiShell = null;
|
||||||
|
platformView.ContentRenderer = null;
|
||||||
|
platformView.ColorRefresher = null;
|
||||||
|
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
VirtualView.Navigating -= OnShellNavigating;
|
||||||
|
VirtualView.Navigated -= OnShellNavigated;
|
||||||
|
}
|
||||||
|
|
||||||
base.DisconnectHandler(platformView);
|
base.DisconnectHandler(platformView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e)
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}");
|
||||||
|
|
||||||
|
// Route to platform view
|
||||||
|
if (PlatformView != null && e.Target?.Location != null)
|
||||||
|
{
|
||||||
|
var route = e.Target.Location.ToString().TrimStart('/');
|
||||||
|
Console.WriteLine($"[ShellHandler] Routing to: {route}");
|
||||||
|
PlatformView.GoToAsync(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncShellItems()
|
||||||
|
{
|
||||||
|
if (PlatformView is null || VirtualView is null || MauiContext is null) return;
|
||||||
|
|
||||||
|
// Clear existing sections
|
||||||
|
foreach (var section in PlatformView.Sections.ToList())
|
||||||
|
{
|
||||||
|
PlatformView.RemoveSection(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add shell items as sections
|
||||||
|
foreach (var item in VirtualView.Items)
|
||||||
|
{
|
||||||
|
if (item is FlyoutItem flyoutItem)
|
||||||
|
{
|
||||||
|
var section = new Platform.ShellSection
|
||||||
|
{
|
||||||
|
Route = flyoutItem.Route ?? flyoutItem.Title ?? "",
|
||||||
|
Title = flyoutItem.Title ?? "",
|
||||||
|
IconPath = flyoutItem.Icon?.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add shell contents as items
|
||||||
|
foreach (var shellSection in flyoutItem.Items)
|
||||||
|
{
|
||||||
|
foreach (var content in shellSection.Items)
|
||||||
|
{
|
||||||
|
var contentItem = new Platform.ShellContent
|
||||||
|
{
|
||||||
|
Route = content.Route ?? content.Title ?? "",
|
||||||
|
Title = content.Title ?? "",
|
||||||
|
IconPath = content.Icon?.ToString(),
|
||||||
|
MauiShellContent = content,
|
||||||
|
Content = RenderShellContent(content)
|
||||||
|
};
|
||||||
|
section.Items.Add(contentItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformView.AddSection(section);
|
||||||
|
}
|
||||||
|
else if (item is ShellItem shellItem)
|
||||||
|
{
|
||||||
|
var section = new Platform.ShellSection
|
||||||
|
{
|
||||||
|
Route = shellItem.Route ?? shellItem.Title ?? "",
|
||||||
|
Title = shellItem.Title ?? "",
|
||||||
|
IconPath = shellItem.Icon?.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var shellSection in shellItem.Items)
|
||||||
|
{
|
||||||
|
foreach (var content in shellSection.Items)
|
||||||
|
{
|
||||||
|
var contentItem = new Platform.ShellContent
|
||||||
|
{
|
||||||
|
Route = content.Route ?? content.Title ?? "",
|
||||||
|
Title = content.Title ?? "",
|
||||||
|
IconPath = content.Icon?.ToString(),
|
||||||
|
MauiShellContent = content,
|
||||||
|
Content = RenderShellContent(content)
|
||||||
|
};
|
||||||
|
section.Items.Add(contentItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformView.AddSection(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SkiaView? RenderShellContent(Microsoft.Maui.Controls.ShellContent content)
|
||||||
|
{
|
||||||
|
if (MauiContext is null) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var page = content.Content as Page;
|
||||||
|
if (page == null && content.ContentTemplate != null)
|
||||||
|
{
|
||||||
|
page = content.ContentTemplate.CreateContent() as Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page != null)
|
||||||
|
{
|
||||||
|
if (page.Handler == null)
|
||||||
|
{
|
||||||
|
page.Handler = page.ToViewHandler(MauiContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.Handler?.PlatformView is SkiaView skiaView)
|
||||||
|
{
|
||||||
|
return skiaView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ShellHandler] Error rendering content: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RefreshShellColors(SkiaShell platformView, Shell shell)
|
||||||
|
{
|
||||||
|
// Sync flyout colors
|
||||||
|
if (shell.FlyoutBackgroundColor is Color flyoutBgColor)
|
||||||
|
{
|
||||||
|
platformView.FlyoutBackgroundColor = flyoutBgColor;
|
||||||
|
}
|
||||||
|
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.MaximumTrackColor)] = MapMaximumTrackColor,
|
||||||
[nameof(ISlider.ThumbColor)] = MapThumbColor,
|
[nameof(ISlider.ThumbColor)] = MapThumbColor,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -48,6 +49,15 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
|||||||
platformView.ValueChanged += OnValueChanged;
|
platformView.ValueChanged += OnValueChanged;
|
||||||
platformView.DragStarted += OnDragStarted;
|
platformView.DragStarted += OnDragStarted;
|
||||||
platformView.DragCompleted += OnDragCompleted;
|
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)
|
protected override void DisconnectHandler(SkiaSlider platformView)
|
||||||
@@ -58,7 +68,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
|
|||||||
base.DisconnectHandler(platformView);
|
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;
|
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;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
// MinimumTrackColor maps to ActiveTrackColor (the filled portion)
|
|
||||||
if (slider.MinimumTrackColor is not null)
|
if (slider.MinimumTrackColor is not null)
|
||||||
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
|
handler.PlatformView.MinimumTrackColor = slider.MinimumTrackColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
|
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
// MaximumTrackColor maps to TrackColor (the unfilled portion)
|
|
||||||
if (slider.MaximumTrackColor is not null)
|
if (slider.MaximumTrackColor is not null)
|
||||||
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
|
handler.PlatformView.MaximumTrackColor = slider.MaximumTrackColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapThumbColor(SliderHandler handler, ISlider slider)
|
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 (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (slider.ThumbColor is not null)
|
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)
|
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)
|
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.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for Stepper on Linux using Skia rendering.
|
/// Handler for Stepper on Linux using Skia rendering.
|
||||||
|
/// Maps IStepper interface to SkiaStepper platform view.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
||||||
{
|
{
|
||||||
@@ -19,7 +20,9 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
|||||||
[nameof(IStepper.Value)] = MapValue,
|
[nameof(IStepper.Value)] = MapValue,
|
||||||
[nameof(IStepper.Minimum)] = MapMinimum,
|
[nameof(IStepper.Minimum)] = MapMinimum,
|
||||||
[nameof(IStepper.Maximum)] = MapMaximum,
|
[nameof(IStepper.Maximum)] = MapMaximum,
|
||||||
|
["Increment"] = MapIncrement,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IStepper, StepperHandler> CommandMapper =
|
public static CommandMapper<IStepper, StepperHandler> CommandMapper =
|
||||||
@@ -45,6 +48,26 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
|||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.ValueChanged += OnValueChanged;
|
platformView.ValueChanged += OnValueChanged;
|
||||||
|
|
||||||
|
// Apply dark theme colors if needed
|
||||||
|
if (Application.Current?.UserAppTheme == AppTheme.Dark)
|
||||||
|
{
|
||||||
|
platformView.ButtonBackgroundColor = Color.FromRgb(66, 66, 66);
|
||||||
|
platformView.ButtonPressedColor = Color.FromRgb(97, 97, 97);
|
||||||
|
platformView.ButtonDisabledColor = Color.FromRgb(48, 48, 48);
|
||||||
|
platformView.SymbolColor = Color.FromRgb(224, 224, 224);
|
||||||
|
platformView.SymbolDisabledColor = Color.FromRgb(97, 97, 97);
|
||||||
|
platformView.BorderColor = Color.FromRgb(97, 97, 97);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync properties
|
||||||
|
if (VirtualView != null)
|
||||||
|
{
|
||||||
|
MapValue(this, VirtualView);
|
||||||
|
MapMinimum(this, VirtualView);
|
||||||
|
MapMaximum(this, VirtualView);
|
||||||
|
MapIsEnabled(this, VirtualView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaStepper platformView)
|
protected override void DisconnectHandler(SkiaStepper platformView)
|
||||||
@@ -53,15 +76,21 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
|||||||
base.DisconnectHandler(platformView);
|
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;
|
if (VirtualView is null || PlatformView is null) return;
|
||||||
VirtualView.Value = PlatformView.Value;
|
|
||||||
|
if (Math.Abs(VirtualView.Value - e.NewValue) > 0.0001)
|
||||||
|
{
|
||||||
|
VirtualView.Value = e.NewValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapValue(StepperHandler handler, IStepper stepper)
|
public static void MapValue(StepperHandler handler, IStepper stepper)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (Math.Abs(handler.PlatformView.Value - stepper.Value) > 0.0001)
|
||||||
handler.PlatformView.Value = stepper.Value;
|
handler.PlatformView.Value = stepper.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +112,24 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
|
|||||||
|
|
||||||
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.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.TrackColor)] = MapTrackColor,
|
||||||
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
|
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -69,13 +70,12 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
|||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
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)
|
if (@switch.TrackColor is not null)
|
||||||
{
|
{
|
||||||
var color = @switch.TrackColor.ToSKColor();
|
handler.PlatformView.OnTrackColor = @switch.TrackColor;
|
||||||
handler.PlatformView.OnTrackColor = color;
|
// Off track is a lighter/desaturated version
|
||||||
// Off track could be a lighter version
|
handler.PlatformView.OffTrackColor = @switch.TrackColor.WithAlpha(0.5f);
|
||||||
handler.PlatformView.OffTrackColor = color.WithAlpha(128);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (@switch.ThumbColor is not null)
|
if (@switch.ThumbColor is not null)
|
||||||
handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor();
|
handler.PlatformView.ThumbColor = @switch.ThumbColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(SwitchHandler handler, ISwitch @switch)
|
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)
|
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.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
@@ -13,8 +15,14 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage>
|
public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage>
|
||||||
{
|
{
|
||||||
|
private bool _isUpdatingSelection;
|
||||||
|
|
||||||
public static IPropertyMapper<ITabbedView, TabbedPageHandler> Mapper = new PropertyMapper<ITabbedView, TabbedPageHandler>(ViewHandler.ViewMapper)
|
public static IPropertyMapper<ITabbedView, TabbedPageHandler> Mapper = new PropertyMapper<ITabbedView, TabbedPageHandler>(ViewHandler.ViewMapper)
|
||||||
{
|
{
|
||||||
|
[nameof(TabbedPage.BarBackgroundColor)] = MapBarBackgroundColor,
|
||||||
|
[nameof(TabbedPage.BarTextColor)] = MapBarTextColor,
|
||||||
|
[nameof(TabbedPage.SelectedTabColor)] = MapSelectedTabColor,
|
||||||
|
[nameof(TabbedPage.UnselectedTabColor)] = MapUnselectedTabColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<ITabbedView, TabbedPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<ITabbedView, TabbedPageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -39,6 +47,9 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
|
|||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
|
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
|
||||||
|
|
||||||
|
// Sync initial tabs
|
||||||
|
SyncTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaTabbedPage platformView)
|
protected override void DisconnectHandler(SkiaTabbedPage platformView)
|
||||||
@@ -50,6 +61,104 @@ public partial class TabbedPageHandler : ViewHandler<ITabbedView, SkiaTabbedPage
|
|||||||
|
|
||||||
private void OnSelectedIndexChanged(object? sender, EventArgs e)
|
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.Format)] = MapFormat,
|
||||||
[nameof(ITimePicker.TextColor)] = MapTextColor,
|
[nameof(ITimePicker.TextColor)] = MapTextColor,
|
||||||
[nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing,
|
[nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
|
[nameof(ITextStyle.Font)] = MapFont,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,6 +48,16 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
|||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.TimeSelected += OnTimeSelected;
|
platformView.TimeSelected += OnTimeSelected;
|
||||||
|
|
||||||
|
// Apply dark theme colors if needed
|
||||||
|
if (Application.Current?.UserAppTheme == AppTheme.Dark)
|
||||||
|
{
|
||||||
|
platformView.ClockBackgroundColor = Color.FromRgb(30, 30, 30);
|
||||||
|
platformView.ClockFaceColor = Color.FromRgb(45, 45, 45);
|
||||||
|
platformView.TextColor = Color.FromRgb(224, 224, 224);
|
||||||
|
platformView.BorderColor = Color.FromRgb(97, 97, 97);
|
||||||
|
platformView.BackgroundColor = Color.FromRgb(45, 45, 45);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DisconnectHandler(SkiaTimePicker platformView)
|
protected override void DisconnectHandler(SkiaTimePicker platformView)
|
||||||
@@ -55,11 +66,11 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
|
|||||||
base.DisconnectHandler(platformView);
|
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;
|
if (VirtualView is null || PlatformView is null) return;
|
||||||
|
|
||||||
VirtualView.Time = PlatformView.Time;
|
VirtualView.Time = e.NewTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapTime(TimePickerHandler handler, ITimePicker timePicker)
|
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 (handler.PlatformView is null) return;
|
||||||
if (timePicker.TextColor is not null)
|
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)
|
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)
|
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)
|
if (timePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
handler.PlatformView.BackgroundColor = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
207
Handlers/WebViewHandler.Linux.cs
Normal file
207
Handlers/WebViewHandler.Linux.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// 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 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()
|
||||||
|
{
|
||||||
|
Console.WriteLine("[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("[WebViewHandler] Handler connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisconnectHandler(LinuxWebView platformView)
|
||||||
|
{
|
||||||
|
platformView.Navigating -= OnNavigating;
|
||||||
|
platformView.Navigated -= OnNavigated;
|
||||||
|
|
||||||
|
base.DisconnectHandler(platformView);
|
||||||
|
Console.WriteLine("[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;
|
||||||
|
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
Console.WriteLine($"[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();
|
||||||
|
Console.WriteLine("[WebViewHandler] GoBack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView?.CanGoForward == true)
|
||||||
|
{
|
||||||
|
handler.PlatformView.GoForward();
|
||||||
|
Console.WriteLine("[WebViewHandler] GoForward");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.Reload();
|
||||||
|
Console.WriteLine("[WebViewHandler] Reload");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
|
||||||
|
{
|
||||||
|
if (args is string script)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.Eval(script);
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
195
Handlers/WebViewHandler.cs
Normal file
195
Handlers/WebViewHandler.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for WebView control on Linux using WebKitGTK.
|
||||||
|
/// </summary>
|
||||||
|
public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
|
||||||
|
{
|
||||||
|
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
|
||||||
|
{
|
||||||
|
[nameof(IWebView.Source)] = MapSource,
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[WebViewHandler] MapSource called");
|
||||||
|
if (handler.PlatformView == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[WebViewHandler] PlatformView is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = webView.Source;
|
||||||
|
Console.WriteLine($"[WebViewHandler] Source type: {source?.GetType().Name ?? "null"}");
|
||||||
|
|
||||||
|
if (source is UrlWebViewSource urlSource)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WebViewHandler] Loading URL: {urlSource.Url}");
|
||||||
|
handler.PlatformView.Source = urlSource.Url ?? "";
|
||||||
|
}
|
||||||
|
else if (source is HtmlWebViewSource htmlSource)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WebViewHandler] Loading HTML ({htmlSource.Html?.Length ?? 0} chars)");
|
||||||
|
Console.WriteLine($"[WebViewHandler] HTML preview: {htmlSource.Html?.Substring(0, Math.Min(100, htmlSource.Html?.Length ?? 0))}...");
|
||||||
|
handler.PlatformView.Html = htmlSource.Html ?? "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,13 +81,20 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
|||||||
|
|
||||||
public static void MapContent(WindowHandler handler, IWindow window)
|
public static void MapContent(WindowHandler handler, IWindow window)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - PlatformView={handler.PlatformView != null}");
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
var content = window.Content;
|
var content = window.Content;
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
|
||||||
if (content?.Handler?.PlatformView is SkiaView skiaContent)
|
if (content?.Handler?.PlatformView is SkiaView skiaContent)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
|
||||||
handler.PlatformView.Content = skiaContent;
|
handler.PlatformView.Content = skiaContent;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapX(WindowHandler handler, IWindow window)
|
public static void MapX(WindowHandler handler, IWindow window)
|
||||||
@@ -141,6 +148,7 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skia window wrapper for Linux display servers.
|
/// Skia window wrapper for Linux display servers.
|
||||||
|
/// Handles rendering of content and popup overlays automatically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkiaWindow
|
public class SkiaWindow
|
||||||
{
|
{
|
||||||
@@ -164,6 +172,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
|
public string Title
|
||||||
{
|
{
|
||||||
get => _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.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Maui.ApplicationModel;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Microsoft.Maui.ApplicationModel.Communication;
|
using Microsoft.Maui.ApplicationModel.Communication;
|
||||||
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Devices;
|
||||||
|
using Microsoft.Maui.Dispatching;
|
||||||
using Microsoft.Maui.Hosting;
|
using Microsoft.Maui.Hosting;
|
||||||
|
using Microsoft.Maui.Networking;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Converters;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Dispatching;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
using Microsoft.Maui.Platform.Linux.Services;
|
using Microsoft.Maui.Platform.Linux.Services;
|
||||||
using Microsoft.Maui.Storage;
|
using Microsoft.Maui.Storage;
|
||||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
using SkiaSharp;
|
||||||
using Microsoft.Maui.Controls;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for configuring MAUI applications for Linux.
|
|
||||||
/// </summary>
|
|
||||||
public static class LinuxMauiAppBuilderExtensions
|
public static class LinuxMauiAppBuilderExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Configures the MAUI application to run on Linux.
|
|
||||||
/// </summary>
|
|
||||||
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
|
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
|
||||||
{
|
{
|
||||||
return builder.UseLinux(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)
|
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure)
|
||||||
{
|
{
|
||||||
var options = new LinuxApplicationOptions();
|
var options = new LinuxApplicationOptions();
|
||||||
configure?.Invoke(options);
|
configure?.Invoke(options);
|
||||||
|
|
||||||
|
// Register dispatcher provider
|
||||||
|
builder.Services.TryAddSingleton<IDispatcherProvider>(LinuxDispatcherProvider.Instance);
|
||||||
|
|
||||||
|
// Register device services
|
||||||
|
builder.Services.TryAddSingleton<IDeviceInfo>(DeviceInfoService.Instance);
|
||||||
|
builder.Services.TryAddSingleton<IDeviceDisplay>(DeviceDisplayService.Instance);
|
||||||
|
builder.Services.TryAddSingleton<IAppInfo>(AppInfoService.Instance);
|
||||||
|
builder.Services.TryAddSingleton<IConnectivity>(ConnectivityService.Instance);
|
||||||
|
|
||||||
// Register platform services
|
// Register platform services
|
||||||
builder.Services.TryAddSingleton<ILauncher, LauncherService>();
|
builder.Services.TryAddSingleton<ILauncher, LauncherService>();
|
||||||
builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
|
builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
|
||||||
@@ -47,51 +54,99 @@ public static class LinuxMauiAppBuilderExtensions
|
|||||||
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
|
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
|
||||||
builder.Services.TryAddSingleton<IEmail, EmailService>();
|
builder.Services.TryAddSingleton<IEmail, EmailService>();
|
||||||
|
|
||||||
|
// Register theming and accessibility services
|
||||||
|
builder.Services.TryAddSingleton<SystemThemeService>();
|
||||||
|
builder.Services.TryAddSingleton<HighContrastService>();
|
||||||
|
|
||||||
|
// Register accessibility service
|
||||||
|
builder.Services.TryAddSingleton<IAccessibilityService>(_ => AccessibilityServiceFactory.Instance);
|
||||||
|
|
||||||
|
// Register input method service
|
||||||
|
builder.Services.TryAddSingleton<IInputMethodService>(_ => InputMethodServiceFactory.Instance);
|
||||||
|
|
||||||
|
// Register font fallback manager
|
||||||
|
builder.Services.TryAddSingleton(_ => FontFallbackManager.Instance);
|
||||||
|
|
||||||
|
// Register additional Linux-specific services
|
||||||
|
builder.Services.TryAddSingleton<FolderPickerService>();
|
||||||
|
builder.Services.TryAddSingleton<NotificationService>();
|
||||||
|
builder.Services.TryAddSingleton<SystemTrayService>();
|
||||||
|
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
|
||||||
|
builder.Services.TryAddSingleton<DragDropService>();
|
||||||
|
|
||||||
|
// Register GTK host service
|
||||||
|
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
|
||||||
|
|
||||||
|
// Register type converters for XAML support
|
||||||
|
RegisterTypeConverters();
|
||||||
|
|
||||||
// Register Linux-specific handlers
|
// Register Linux-specific handlers
|
||||||
builder.ConfigureMauiHandlers(handlers =>
|
builder.ConfigureMauiHandlers(handlers =>
|
||||||
{
|
{
|
||||||
// Phase 1 - MVP controls
|
// Application handler
|
||||||
handlers.AddHandler<IButton, ButtonHandler>();
|
handlers.AddHandler<IApplication, ApplicationHandler>();
|
||||||
handlers.AddHandler<ILabel, LabelHandler>();
|
|
||||||
handlers.AddHandler<IEntry, EntryHandler>();
|
|
||||||
handlers.AddHandler<ICheckBox, CheckBoxHandler>();
|
|
||||||
handlers.AddHandler<ILayout, LayoutHandler>();
|
|
||||||
handlers.AddHandler<IStackLayout, StackLayoutHandler>();
|
|
||||||
handlers.AddHandler<IGridLayout, GridHandler>();
|
|
||||||
|
|
||||||
// Phase 2 - Input controls
|
// Core controls
|
||||||
handlers.AddHandler<ISlider, SliderHandler>();
|
handlers.AddHandler<BoxView, BoxViewHandler>();
|
||||||
handlers.AddHandler<ISwitch, SwitchHandler>();
|
handlers.AddHandler<Button, TextButtonHandler>();
|
||||||
handlers.AddHandler<IProgress, ProgressBarHandler>();
|
handlers.AddHandler<Label, LabelHandler>();
|
||||||
handlers.AddHandler<IActivityIndicator, ActivityIndicatorHandler>();
|
handlers.AddHandler<Entry, EntryHandler>();
|
||||||
handlers.AddHandler<ISearchBar, SearchBarHandler>();
|
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
|
// Layout controls
|
||||||
handlers.AddHandler<IImage, ImageHandler>();
|
handlers.AddHandler<Grid, GridHandler>();
|
||||||
handlers.AddHandler<IImageButton, ImageButtonHandler>();
|
handlers.AddHandler<StackLayout, StackLayoutHandler>();
|
||||||
handlers.AddHandler<IGraphicsView, GraphicsViewHandler>();
|
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<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<Page, PageHandler>();
|
||||||
handlers.AddHandler<ContentPage, ContentPageHandler>();
|
handlers.AddHandler<ContentPage, ContentPageHandler>();
|
||||||
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
|
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
|
||||||
|
handlers.AddHandler<Shell, ShellHandler>();
|
||||||
|
handlers.AddHandler<FlyoutPage, FlyoutPageHandler>();
|
||||||
|
handlers.AddHandler<TabbedPage, TabbedPageHandler>();
|
||||||
|
|
||||||
// Phase 5 - Advanced Controls
|
// Application & Window
|
||||||
handlers.AddHandler<IPicker, PickerHandler>();
|
handlers.AddHandler<Application, ApplicationHandler>();
|
||||||
handlers.AddHandler<IDatePicker, DatePickerHandler>();
|
handlers.AddHandler<Microsoft.Maui.Controls.Window, WindowHandler>();
|
||||||
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>();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store options for later use
|
// Store options for later use
|
||||||
@@ -99,22 +154,12 @@ public static class LinuxMauiAppBuilderExtensions
|
|||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
private static void RegisterTypeConverters()
|
||||||
/// Handler registration extensions.
|
|
||||||
/// </summary>
|
|
||||||
public static class HandlerMappingExtensions
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter)));
|
||||||
/// Adds a handler for the specified view type.
|
TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter)));
|
||||||
/// </summary>
|
TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter)));
|
||||||
public static IMauiHandlersCollection AddHandler<TView, THandler>(
|
TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter)));
|
||||||
this IMauiHandlersCollection handlers)
|
|
||||||
where TView : class
|
|
||||||
where THandler : class
|
|
||||||
{
|
|
||||||
handlers.AddHandler(typeof(TView), typeof(THandler));
|
|
||||||
return handlers;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Maui.Hosting;
|
|
||||||
using Microsoft.Maui.Controls;
|
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;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point for running MAUI applications on Linux.
|
||||||
|
/// </summary>
|
||||||
public static class LinuxProgramHost
|
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()
|
public static void Run<TApp>(string[] args) where TApp : class, IApplication, new()
|
||||||
{
|
{
|
||||||
Run<TApp>(args, null);
|
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()
|
public static void Run<TApp>(string[] args, Action<MauiAppBuilder>? configure) where TApp : class, IApplication, new()
|
||||||
{
|
{
|
||||||
|
// Build the MAUI application
|
||||||
var builder = MauiApp.CreateBuilder();
|
var builder = MauiApp.CreateBuilder();
|
||||||
builder.UseLinux();
|
builder.UseLinux();
|
||||||
configure?.Invoke(builder);
|
configure?.Invoke(builder);
|
||||||
builder.UseMauiApp<TApp>();
|
builder.UseMauiApp<TApp>();
|
||||||
var mauiApp = builder.Build();
|
var mauiApp = builder.Build();
|
||||||
|
|
||||||
|
// Get application options
|
||||||
var options = mauiApp.Services.GetService<LinuxApplicationOptions>()
|
var options = mauiApp.Services.GetService<LinuxApplicationOptions>()
|
||||||
?? new LinuxApplicationOptions();
|
?? new LinuxApplicationOptions();
|
||||||
ParseCommandLineOptions(args, options);
|
ParseCommandLineOptions(args, options);
|
||||||
|
|
||||||
|
// Initialize GTK for WebView support
|
||||||
|
GtkHostService.Instance.Initialize(options.Title ?? "MAUI Application", options.Width, options.Height);
|
||||||
|
Console.WriteLine("[LinuxProgramHost] GTK initialized for WebView support");
|
||||||
|
|
||||||
|
// Create Linux application
|
||||||
using var linuxApp = new LinuxApplication();
|
using var linuxApp = new LinuxApplication();
|
||||||
linuxApp.Initialize(options);
|
linuxApp.Initialize(options);
|
||||||
|
|
||||||
// Create comprehensive demo UI with ALL controls
|
// Create MAUI context
|
||||||
var rootView = CreateComprehensiveDemo();
|
var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp);
|
||||||
linuxApp.RootView = rootView;
|
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
Console.WriteLine("No application page found. Showing demo UI.");
|
||||||
|
rootView = CreateDemoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxApp.RootView = rootView;
|
||||||
linuxApp.Run();
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error rendering application: {ex.Message}");
|
||||||
|
Console.WriteLine(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)
|
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
@@ -54,11 +172,18 @@ public static class LinuxProgramHost
|
|||||||
options.Height = h;
|
options.Height = h;
|
||||||
i++;
|
i++;
|
||||||
break;
|
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
|
// Create scrollable container
|
||||||
var scroll = new SkiaScrollView();
|
var scroll = new SkiaScrollView();
|
||||||
@@ -67,33 +192,33 @@ public static class LinuxProgramHost
|
|||||||
{
|
{
|
||||||
Orientation = StackOrientation.Vertical,
|
Orientation = StackOrientation.Vertical,
|
||||||
Spacing = 15,
|
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 ==========
|
// ========== TITLE ==========
|
||||||
root.AddChild(new SkiaLabel
|
root.AddChild(new SkiaLabel
|
||||||
{
|
{
|
||||||
Text = "MAUI Linux Control Demo",
|
Text = "OpenMaui Linux Control Demo",
|
||||||
FontSize = 28,
|
FontSize = 28,
|
||||||
TextColor = new SKColor(0x1A, 0x23, 0x7E),
|
TextColor = Color.FromRgb(0x1A, 0x23, 0x7E),
|
||||||
IsBold = true
|
FontAttributes = FontAttributes.Bold
|
||||||
});
|
});
|
||||||
root.AddChild(new SkiaLabel
|
root.AddChild(new SkiaLabel
|
||||||
{
|
{
|
||||||
Text = "All controls rendered using SkiaSharp on X11",
|
Text = "All controls rendered using SkiaSharp on X11",
|
||||||
FontSize = 14,
|
FontSize = 14,
|
||||||
TextColor = SKColors.Gray
|
TextColor = Colors.Gray
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== LABELS SECTION ==========
|
// ========== LABELS SECTION ==========
|
||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
root.AddChild(CreateSectionHeader("Labels"));
|
root.AddChild(CreateSectionHeader("Labels"));
|
||||||
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
|
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
|
||||||
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black });
|
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = Colors.Black });
|
||||||
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true });
|
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 = SKColors.Gray, IsItalic = true });
|
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 = new SKColor(0xE9, 0x1E, 0x63) });
|
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = Color.FromRgb(0xE9, 0x1E, 0x63) });
|
||||||
root.AddChild(labelSection);
|
root.AddChild(labelSection);
|
||||||
|
|
||||||
// ========== BUTTONS SECTION ==========
|
// ========== BUTTONS SECTION ==========
|
||||||
@@ -102,20 +227,20 @@ public static class LinuxProgramHost
|
|||||||
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||||
|
|
||||||
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
|
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
|
||||||
btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3);
|
btnPrimary.BackgroundColor = Color.FromRgb(0x21, 0x96, 0xF3);
|
||||||
btnPrimary.TextColor = SKColors.White;
|
btnPrimary.TextColor = Colors.White;
|
||||||
var clickCount = 0;
|
var clickCount = 0;
|
||||||
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
|
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
|
||||||
buttonSection.AddChild(btnPrimary);
|
buttonSection.AddChild(btnPrimary);
|
||||||
|
|
||||||
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
|
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
|
||||||
btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50);
|
btnSuccess.BackgroundColor = Color.FromRgb(0x4C, 0xAF, 0x50);
|
||||||
btnSuccess.TextColor = SKColors.White;
|
btnSuccess.TextColor = Colors.White;
|
||||||
buttonSection.AddChild(btnSuccess);
|
buttonSection.AddChild(btnSuccess);
|
||||||
|
|
||||||
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
|
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
|
||||||
btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36);
|
btnDanger.BackgroundColor = Color.FromRgb(0xF4, 0x43, 0x36);
|
||||||
btnDanger.TextColor = SKColors.White;
|
btnDanger.TextColor = Colors.White;
|
||||||
buttonSection.AddChild(btnDanger);
|
buttonSection.AddChild(btnDanger);
|
||||||
|
|
||||||
root.AddChild(buttonSection);
|
root.AddChild(buttonSection);
|
||||||
@@ -130,7 +255,7 @@ public static class LinuxProgramHost
|
|||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
root.AddChild(CreateSectionHeader("SearchBar"));
|
root.AddChild(CreateSectionHeader("SearchBar"));
|
||||||
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
|
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
|
||||||
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray };
|
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = Colors.Gray };
|
||||||
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
|
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
|
||||||
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
|
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
|
||||||
root.AddChild(searchBar);
|
root.AddChild(searchBar);
|
||||||
@@ -143,7 +268,7 @@ public static class LinuxProgramHost
|
|||||||
{
|
{
|
||||||
Placeholder = "Enter multiple lines of text...",
|
Placeholder = "Enter multiple lines of text...",
|
||||||
FontSize = 14,
|
FontSize = 14,
|
||||||
BackgroundColor = SKColors.White
|
BackgroundColor = Colors.White
|
||||||
};
|
};
|
||||||
root.AddChild(editor);
|
root.AddChild(editor);
|
||||||
|
|
||||||
@@ -205,7 +330,7 @@ public static class LinuxProgramHost
|
|||||||
root.AddChild(CreateSectionHeader("ProgressBar"));
|
root.AddChild(CreateSectionHeader("ProgressBar"));
|
||||||
var progress = new SkiaProgressBar { Progress = 0.7f };
|
var progress = new SkiaProgressBar { Progress = 0.7f };
|
||||||
root.AddChild(progress);
|
root.AddChild(progress);
|
||||||
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray });
|
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = Colors.Gray });
|
||||||
|
|
||||||
// ========== ACTIVITYINDICATOR SECTION ==========
|
// ========== ACTIVITYINDICATOR SECTION ==========
|
||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
@@ -213,7 +338,7 @@ public static class LinuxProgramHost
|
|||||||
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
|
||||||
var activity = new SkiaActivityIndicator { IsRunning = true };
|
var activity = new SkiaActivityIndicator { IsRunning = true };
|
||||||
activitySection.AddChild(activity);
|
activitySection.AddChild(activity);
|
||||||
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray });
|
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = Colors.Gray });
|
||||||
root.AddChild(activitySection);
|
root.AddChild(activitySection);
|
||||||
|
|
||||||
// ========== PICKER SECTION ==========
|
// ========== PICKER SECTION ==========
|
||||||
@@ -221,7 +346,7 @@ public static class LinuxProgramHost
|
|||||||
root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
|
root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
|
||||||
var picker = new SkiaPicker { Title = "Select an item" };
|
var picker = new SkiaPicker { Title = "Select an item" };
|
||||||
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
|
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
|
||||||
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
|
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
|
||||||
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
|
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
|
||||||
root.AddChild(picker);
|
root.AddChild(picker);
|
||||||
root.AddChild(pickerLabel);
|
root.AddChild(pickerLabel);
|
||||||
@@ -230,7 +355,7 @@ public static class LinuxProgramHost
|
|||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
root.AddChild(CreateSectionHeader("DatePicker"));
|
root.AddChild(CreateSectionHeader("DatePicker"));
|
||||||
var datePicker = new SkiaDatePicker { Date = DateTime.Today };
|
var datePicker = new SkiaDatePicker { Date = DateTime.Today };
|
||||||
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = 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}";
|
datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}";
|
||||||
root.AddChild(datePicker);
|
root.AddChild(datePicker);
|
||||||
root.AddChild(dateLabel);
|
root.AddChild(dateLabel);
|
||||||
@@ -239,7 +364,7 @@ public static class LinuxProgramHost
|
|||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
root.AddChild(CreateSectionHeader("TimePicker"));
|
root.AddChild(CreateSectionHeader("TimePicker"));
|
||||||
var timePicker = new SkiaTimePicker();
|
var timePicker = new SkiaTimePicker();
|
||||||
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = 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}";
|
timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}";
|
||||||
root.AddChild(timePicker);
|
root.AddChild(timePicker);
|
||||||
root.AddChild(timeLabel);
|
root.AddChild(timeLabel);
|
||||||
@@ -251,18 +376,18 @@ public static class LinuxProgramHost
|
|||||||
{
|
{
|
||||||
CornerRadius = 8,
|
CornerRadius = 8,
|
||||||
StrokeThickness = 2,
|
StrokeThickness = 2,
|
||||||
Stroke = new SKColor(0x21, 0x96, 0xF3),
|
Stroke = Color.FromRgb(0x21, 0x96, 0xF3),
|
||||||
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD)
|
BackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD)
|
||||||
};
|
};
|
||||||
border.SetPadding(15);
|
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);
|
root.AddChild(border);
|
||||||
|
|
||||||
// ========== FRAME SECTION ==========
|
// ========== FRAME SECTION ==========
|
||||||
root.AddChild(CreateSeparator());
|
root.AddChild(CreateSeparator());
|
||||||
root.AddChild(CreateSectionHeader("Frame (with shadow)"));
|
root.AddChild(CreateSectionHeader("Frame (with shadow)"));
|
||||||
var frame = new SkiaFrame();
|
var frame = new SkiaFrame();
|
||||||
frame.BackgroundColor = SKColors.White;
|
frame.BackgroundColor = Colors.White;
|
||||||
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
|
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
|
||||||
root.AddChild(frame);
|
root.AddChild(frame);
|
||||||
|
|
||||||
@@ -276,7 +401,7 @@ public static class LinuxProgramHost
|
|||||||
Footer = "End of list"
|
Footer = "End of list"
|
||||||
};
|
};
|
||||||
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
|
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
|
||||||
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
|
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
|
||||||
collectionView.SelectionChanged += (s, e) =>
|
collectionView.SelectionChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
var selected = e.CurrentSelection.FirstOrDefault();
|
var selected = e.CurrentSelection.FirstOrDefault();
|
||||||
@@ -294,18 +419,15 @@ public static class LinuxProgramHost
|
|||||||
var imgBtn = new SkiaImageButton
|
var imgBtn = new SkiaImageButton
|
||||||
{
|
{
|
||||||
CornerRadius = 8,
|
CornerRadius = 8,
|
||||||
StrokeColor = new SKColor(0x21, 0x96, 0xF3),
|
StrokeColor = Color.FromRgb(0x21, 0x96, 0xF3),
|
||||||
StrokeThickness = 1,
|
StrokeThickness = 1,
|
||||||
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD),
|
ImageBackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD),
|
||||||
PaddingLeft = 10,
|
Padding = new Thickness(10)
|
||||||
PaddingRight = 10,
|
|
||||||
PaddingTop = 10,
|
|
||||||
PaddingBottom = 10
|
|
||||||
};
|
};
|
||||||
// Generate a simple star icon bitmap
|
// Generate a simple star icon bitmap
|
||||||
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
|
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
|
||||||
imgBtn.Bitmap = iconBitmap;
|
imgBtn.Bitmap = iconBitmap;
|
||||||
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray };
|
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = Colors.Gray };
|
||||||
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
|
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
|
||||||
imageButtonSection.AddChild(imgBtn);
|
imageButtonSection.AddChild(imgBtn);
|
||||||
imageButtonSection.AddChild(imgBtnLabel);
|
imageButtonSection.AddChild(imgBtnLabel);
|
||||||
@@ -321,7 +443,7 @@ public static class LinuxProgramHost
|
|||||||
var sampleBitmap = CreateSampleImage(80, 60);
|
var sampleBitmap = CreateSampleImage(80, 60);
|
||||||
img.Bitmap = sampleBitmap;
|
img.Bitmap = sampleBitmap;
|
||||||
imageSection.AddChild(img);
|
imageSection.AddChild(img);
|
||||||
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray });
|
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = Colors.Gray });
|
||||||
root.AddChild(imageSection);
|
root.AddChild(imageSection);
|
||||||
|
|
||||||
// ========== FOOTER ==========
|
// ========== FOOTER ==========
|
||||||
@@ -330,14 +452,14 @@ public static class LinuxProgramHost
|
|||||||
{
|
{
|
||||||
Text = "All 25+ controls are interactive - try them all!",
|
Text = "All 25+ controls are interactive - try them all!",
|
||||||
FontSize = 16,
|
FontSize = 16,
|
||||||
TextColor = new SKColor(0x4C, 0xAF, 0x50),
|
TextColor = Color.FromRgb(0x4C, 0xAF, 0x50),
|
||||||
IsBold = true
|
FontAttributes = FontAttributes.Bold
|
||||||
});
|
});
|
||||||
root.AddChild(new SkiaLabel
|
root.AddChild(new SkiaLabel
|
||||||
{
|
{
|
||||||
Text = "Scroll down to see more controls",
|
Text = "Scroll down to see more controls",
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
TextColor = SKColors.Gray
|
TextColor = Colors.Gray
|
||||||
});
|
});
|
||||||
|
|
||||||
scroll.Content = root;
|
scroll.Content = root;
|
||||||
@@ -350,14 +472,14 @@ public static class LinuxProgramHost
|
|||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
FontSize = 18,
|
FontSize = 18,
|
||||||
TextColor = new SKColor(0x37, 0x47, 0x4F),
|
TextColor = Color.FromRgb(0x37, 0x47, 0x4F),
|
||||||
IsBold = true
|
FontAttributes = FontAttributes.Bold
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SkiaView CreateSeparator()
|
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;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
563
Hosting/LinuxViewRenderer.cs
Normal file
563
Hosting/LinuxViewRenderer.cs
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
// 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 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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the route - remove leading // or /
|
||||||
|
var cleanRoute = route.TrimStart('/');
|
||||||
|
Console.WriteLine($"[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))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}");
|
||||||
|
CurrentSkiaShell.NavigateToSection(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}");
|
||||||
|
|
||||||
|
if (CurrentSkiaShell == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PushPage] CurrentSkiaShell is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurrentRenderer == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PushPage] Failed to render page through handler");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push onto SkiaShell's navigation stack
|
||||||
|
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail");
|
||||||
|
Console.WriteLine($"[PushPage] Successfully pushed page via handler system");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PushPage] Error: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pops the current page from the navigation stack.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if successful</returns>
|
||||||
|
public static bool PopPage()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PopPage] Popping page");
|
||||||
|
|
||||||
|
if (CurrentSkiaShell == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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) => Console.WriteLine($"[Navigation] Navigating: {e.Target}");
|
||||||
|
|
||||||
|
Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
|
||||||
|
for (int i = 0; i < skiaShell.Sections.Count; i++)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Navigation] 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;
|
||||||
|
Console.WriteLine($"[ApplyShellColors] Theme is: {(isDark ? "Dark" : "Light")}");
|
||||||
|
|
||||||
|
// Flyout background color
|
||||||
|
if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent)
|
||||||
|
{
|
||||||
|
skiaShell.FlyoutBackgroundColor = shell.FlyoutBackgroundColor;
|
||||||
|
Console.WriteLine($"[ApplyShellColors] FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skiaShell.FlyoutBackgroundColor = isDark
|
||||||
|
? Color.FromRgb(30, 30, 30)
|
||||||
|
: Color.FromRgb(255, 255, 255);
|
||||||
|
Console.WriteLine($"[ApplyShellColors] Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flyout text color
|
||||||
|
skiaShell.FlyoutTextColor = isDark
|
||||||
|
? Color.FromRgb(224, 224, 224)
|
||||||
|
: Color.FromRgb(33, 33, 33);
|
||||||
|
Console.WriteLine($"[ApplyShellColors] FlyoutTextColor: {skiaShell.FlyoutTextColor}");
|
||||||
|
|
||||||
|
// Content background color
|
||||||
|
skiaShell.ContentBackgroundColor = isDark
|
||||||
|
? Color.FromRgb(18, 18, 18)
|
||||||
|
: Color.FromRgb(250, 250, 250);
|
||||||
|
Console.WriteLine($"[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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Navigation] OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}");
|
||||||
|
|
||||||
|
if (CurrentSkiaShell == null || CurrentMauiShell == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Navigation] CurrentSkiaShell or CurrentMauiShell is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current route from the Shell
|
||||||
|
var currentState = CurrentMauiShell.CurrentState;
|
||||||
|
var location = currentState?.Location?.OriginalString ?? "";
|
||||||
|
Console.WriteLine($"[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];
|
||||||
|
Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
|
||||||
|
if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[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))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}");
|
||||||
|
if (i != CurrentSkiaShell.CurrentSectionIndex)
|
||||||
|
{
|
||||||
|
CurrentSkiaShell.NavigateToSection(i);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[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;
|
||||||
|
Console.WriteLine($"[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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
123
Hosting/MauiHandlerExtensions.cs
Normal file
123
Hosting/MauiHandlerExtensions.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
|
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();
|
||||||
|
Console.WriteLine($"[ToHandler] 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();
|
||||||
|
Console.WriteLine($"[ToHandler] 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);
|
||||||
|
Console.WriteLine($"[ToHandler] 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;
|
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;
|
||||||
|
}
|
||||||
25
Interop/XCrossingEvent.cs
Normal file
25
Interop/XCrossingEvent.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.
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Interop;
|
||||||
|
|
||||||
|
public struct XCrossingEvent
|
||||||
|
{
|
||||||
|
public int Type;
|
||||||
|
public ulong Serial;
|
||||||
|
public int SendEvent;
|
||||||
|
public IntPtr Display;
|
||||||
|
public IntPtr Window;
|
||||||
|
public IntPtr Root;
|
||||||
|
public IntPtr Subwindow;
|
||||||
|
public ulong Time;
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
public int XRoot;
|
||||||
|
public int YRoot;
|
||||||
|
public int Mode;
|
||||||
|
public int Detail;
|
||||||
|
public int SameScreen;
|
||||||
|
public int Focus;
|
||||||
|
public uint State;
|
||||||
|
}
|
||||||
13
Interop/XCursorShape.cs
Normal file
13
Interop/XCursorShape.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.
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Interop;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
37
Interop/XEvent.cs
Normal file
37
Interop/XEvent.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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, 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;
|
||||||
|
}
|
||||||
18
Interop/XEventMask.cs
Normal file
18
Interop/XEventMask.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.Interop;
|
||||||
|
|
||||||
|
public static class XEventMask
|
||||||
|
{
|
||||||
|
public const long KeyPressMask = 1L;
|
||||||
|
public const long KeyReleaseMask = 2L;
|
||||||
|
public const long ButtonPressMask = 4L;
|
||||||
|
public const long ButtonReleaseMask = 8L;
|
||||||
|
public const long EnterWindowMask = 16L;
|
||||||
|
public const long LeaveWindowMask = 32L;
|
||||||
|
public const long PointerMotionMask = 64L;
|
||||||
|
public const long ExposureMask = 32768L;
|
||||||
|
public const long StructureNotifyMask = 131072L;
|
||||||
|
public const long FocusChangeMask = 2097152L;
|
||||||
|
}
|
||||||
20
Interop/XEventType.cs
Normal file
20
Interop/XEventType.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.
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Interop;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
18
Interop/XExposeEvent.cs
Normal file
18
Interop/XExposeEvent.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.Interop;
|
||||||
|
|
||||||
|
public struct XExposeEvent
|
||||||
|
{
|
||||||
|
public int Type;
|
||||||
|
public ulong Serial;
|
||||||
|
public int SendEvent;
|
||||||
|
public IntPtr Display;
|
||||||
|
public IntPtr Window;
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
public int Width;
|
||||||
|
public int Height;
|
||||||
|
public int Count;
|
||||||
|
}
|
||||||
15
Interop/XFocusChangeEvent.cs
Normal file
15
Interop/XFocusChangeEvent.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// 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 XFocusChangeEvent
|
||||||
|
{
|
||||||
|
public int Type;
|
||||||
|
public ulong Serial;
|
||||||
|
public int SendEvent;
|
||||||
|
public IntPtr Display;
|
||||||
|
public IntPtr Window;
|
||||||
|
public int Mode;
|
||||||
|
public int Detail;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user