Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6a3f4acc4 | |||
| 8a5ad459ed | |||
| 840ad0ce8f | |||
| 967713c01a | |||
| 6b45f28b4e | |||
| 3412cb982e | |||
| c5221ba580 | |||
| 077abc2feb | |||
| ee60b983a4 | |||
| 0fb0051a24 | |||
| 15ced2ac55 | |||
| e55230c441 | |||
| 08e0c4d2b9 | |||
| d9d3218f17 | |||
| 996ffb67c0 | |||
| 4d880e77d5 | |||
| ade3a8a47d | |||
| b7424194f4 |
@@ -1,46 +1,39 @@
|
||||
# OpenMaui Linux CI/CD Pipeline for Gitea
|
||||
# OpenMaui Linux CI Pipeline
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
branches: [main, final, 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
|
||||
build-linux:
|
||||
name: Build (Linux)
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore dependencies
|
||||
run: C:\dotnet\dotnet.exe restore
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
|
||||
run: dotnet 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: Pack NuGet
|
||||
run: dotnet 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 }}
|
||||
- name: Upload NuGet packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: ./nupkg/*.nupkg
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# OpenMaui Linux Release - Publish to NuGet
|
||||
name: Release to NuGet
|
||||
# OpenMaui Linux Release - Publish to Package Registries
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
@@ -11,31 +9,59 @@ on:
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_ROOT: C:\dotnet
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Publish to NuGet
|
||||
runs-on: windows
|
||||
name: Build and Publish
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
VERSION="${TAG#v}"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Restore dependencies
|
||||
run: C:\dotnet\dotnet.exe restore
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
|
||||
run: dotnet 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
|
||||
run: dotnet pack --configuration Release --no-build -o ./nupkg -p:PackageVersion=${{ steps.version.outputs.VERSION }}
|
||||
|
||||
- name: Upload NuGet packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: ./nupkg/*.nupkg
|
||||
|
||||
- name: Publish to Gitea Packages
|
||||
run: |
|
||||
for pkg in ./nupkg/*.nupkg; do
|
||||
dotnet nuget push "$pkg" \
|
||||
--api-key ${{ secrets.GITEATOKEN }} \
|
||||
--source https://git.marketally.ai/api/packages/${{ gitea.repository_owner }}/nuget/index.json \
|
||||
--skip-duplicate
|
||||
done
|
||||
|
||||
- name: Publish to NuGet.org
|
||||
if: ${{ secrets.NUGET_API_KEY != '' }}
|
||||
run: |
|
||||
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
|
||||
}
|
||||
for pkg in ./nupkg/*.nupkg; do
|
||||
dotnet nuget push "$pkg" \
|
||||
--api-key ${{ secrets.NUGET_API_KEY }} \
|
||||
--source https://api.nuget.org/v3/index.json \
|
||||
--skip-duplicate
|
||||
done
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,3 +48,7 @@ coverage*.xml
|
||||
# Publish output
|
||||
publish/
|
||||
mauiplan.md
|
||||
|
||||
# Development artifacts
|
||||
out.xml
|
||||
MERGE_TRACKING.md
|
||||
|
||||
8
.notes/note-1770523035702-znjlto6vn.json
Normal file
8
.notes/note-1770523035702-znjlto6vn.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "note-1770523035702-znjlto6vn",
|
||||
"title": "check later",
|
||||
"content": "● Review in OpenMaui:\n\n | Feature Used | OpenMaui Support | Notes |\n |--------------------------------|------------------|-------------------------------|\n | Controls | | |\n | ContentPage | ✓ | SkiaContentPage |\n | Grid | ✓ | SkiaGrid |\n | VerticalStackLayout | ✓ | SkiaStackLayout |\n | HorizontalStackLayout | ✓ | SkiaStackLayout |\n | Label | ✓ | SkiaLabel |\n | Button | ✓ | SkiaButton |\n | CheckBox | ✓ | SkiaCheckBox |\n | ProgressBar | ✓ | SkiaProgressBar |\n | Border | ✓ | SkiaBorder |\n | CollectionView | ✓ | SkiaCollectionView |\n | XAML Features | | |\n | Data Binding | ✓ | Full BindableProperty support |\n | StaticResource | ✓ | ResourceDictionary |\n | DataTemplate | ✓ | |\n | IValueConverter | ✓ | |\n | Potential Gaps | | |\n | StrokeShape=\"RoundRectangle 8\" | ? | May need different syntax |\n | FormattedText / Span | ? | Didn't see explicit support |\n | x:DataType compiled bindings | ? | May work, may not |\n | CollectionView.EmptyView | ? | Not confirmed |\n\n Honest assessment: The core controls are there, but I'd need to check:\n\n 1. Does SkiaBorder support StrokeShape with corner radius?\n 2. Does SkiaLabel support FormattedText with multiple Span elements?\n 3. Does SkiaCollectionView support EmptyView?\n\n Want me to dig into the OpenMaui source to verify these specific features?",
|
||||
"createdAt": 1770523035699,
|
||||
"updatedAt": 1771217844920,
|
||||
"tags": []
|
||||
}
|
||||
55
.notes/series-1769749919894-6e18cc.json
Normal file
55
.notes/series-1769749919894-6e18cc.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750134166-c6e172.json
Normal file
55
.notes/series-1769750134166-c6e172.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750314106-d25473.json
Normal file
55
.notes/series-1769750314106-d25473.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750550451-66bac3.json
Normal file
55
.notes/series-1769750550451-66bac3.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769750791920-5db729.json
Normal file
55
.notes/series-1769750791920-5db729.json
Normal file
File diff suppressed because one or more lines are too long
55
.notes/series-1769751056430-116f9a.json
Normal file
55
.notes/series-1769751056430-116f9a.json
Normal file
File diff suppressed because one or more lines are too long
59
.notes/series-1769751324711-9007af.json
Normal file
59
.notes/series-1769751324711-9007af.json
Normal file
File diff suppressed because one or more lines are too long
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
Version numbers are aligned with .NET / MAUI versions (e.g., OpenMaui 9.0.x targets .NET 9 / MAUI 9).
|
||||
|
||||
## [9.0.40] - 2026-03-07
|
||||
|
||||
> Version aligned with MAUI 9.0.40. Previously released as 1.0.0.
|
||||
|
||||
### Added
|
||||
- 35+ Skia-rendered controls: Button, Label, Entry, Editor, CheckBox, Switch, RadioButton, Slider, Stepper, Picker, DatePicker, TimePicker, SearchBar, Image, ImageButton, ProgressBar, ActivityIndicator, BoxView, Border, Frame, ScrollView, CollectionView, CarouselView, IndicatorView, SwipeView, RefreshView, GraphicsView, WebView, MenuBar
|
||||
- Navigation: NavigationPage, TabbedPage, FlyoutPage, Shell
|
||||
- Full XAML support with BindableProperty for all controls
|
||||
- Visual State Manager integration (Normal, PointerOver, Pressed, Focused, Disabled)
|
||||
- Data binding (OneWay, TwoWay, OneTime) with IValueConverter support
|
||||
- XAML styles, StaticResource, DynamicResource, merged ResourceDictionaries
|
||||
- X11 display server support with full input handling
|
||||
- Wayland support with XWayland fallback
|
||||
- SkiaSharp hardware-accelerated rendering with dirty region optimization
|
||||
- AT-SPI2 accessibility support (screen reader integration)
|
||||
- High contrast mode detection and color palette support
|
||||
- Input method support (IBus, Fcitx5, XIM)
|
||||
- HiDPI automatic scale factor detection (GNOME, KDE, X11)
|
||||
- Platform services: Clipboard, FilePicker, FolderPicker, Notifications, GlobalHotkeys, DragDrop, Launcher, Share, SecureStorage, Preferences, Browser, Email, SystemTray, VersionTracking, AppActions
|
||||
- Gesture recognition: Tap, Pan, Swipe, Pinch, Pointer, Drag/Drop
|
||||
- Project templates: `openmaui-linux` (code-based) and `openmaui-linux-xaml` (XAML-based)
|
||||
- Visual Studio extension with project templates and launch profiles
|
||||
- DiagnosticLog centralized logging infrastructure (conditional on DEBUG builds)
|
||||
- Configurable gesture thresholds (SwipeMinDistance, SwipeMaxTime, etc.)
|
||||
- Exception-safe rendering pipeline
|
||||
- SafeHandle wrappers for native interop (GTK, X11, GObject)
|
||||
- Performance benchmarks for rendering pipeline (541 passing tests)
|
||||
- Threading model and DI migration documentation
|
||||
|
||||
### Fixed
|
||||
- Native resource leaks: GTK signal disconnection, X11 cursor freeing, CSS provider unref, WebKit dlclose
|
||||
- 27 empty catch blocks replaced with DiagnosticLog for debuggability
|
||||
- GestureManager memory leak (view tracking dictionaries now cleaned up on dispose)
|
||||
- Text binding recursion guard in EntryHandler
|
||||
- Rendering pipeline crash protection (exceptions in view Draw no longer crash the app)
|
||||
|
||||
## [1.0.0] - 2026-03-06 [DEPRECATED]
|
||||
|
||||
> Superseded by 9.0.40. Identical codebase, version renumbered to align with MAUI versioning.
|
||||
|
||||
## [1.0.0-rc.1] - 2026-02-01
|
||||
|
||||
### Added
|
||||
- 100% .NET MAUI API compliance - all public APIs use MAUI types
|
||||
- 217 passing unit tests
|
||||
|
||||
## [1.0.0-preview.1] - 2025-06-01
|
||||
|
||||
### Added
|
||||
- Initial preview release
|
||||
- Core rendering engine
|
||||
- Basic control set
|
||||
297
CLAUDE.md
297
CLAUDE.md
@@ -1,297 +0,0 @@
|
||||
# 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
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to .NET MAUI Linux Platform
|
||||
|
||||
Thank you for your interest in contributing to the .NET MAUI Linux Platform! This project is developed and maintained by [MarketAlly LLC](https://marketally.com) under the leadership of David H. Friedel Jr.
|
||||
Thank you for your interest in contributing to the .NET MAUI Linux Platform! This project is developed and maintained by [MarketAlly Pte Ltd](https://marketally.sg) under the leadership of David H. Friedel Jr.
|
||||
|
||||
This document provides guidelines and information for contributors.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Microsoft.Maui.Dispatching;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Dispatching;
|
||||
|
||||
@@ -24,7 +25,7 @@ public class LinuxDispatcher : IDispatcher
|
||||
{
|
||||
_mainThreadId = Environment.CurrentManagedThreadId;
|
||||
_mainDispatcher = new LinuxDispatcher();
|
||||
Console.WriteLine($"[LinuxDispatcher] Initialized on thread {_mainThreadId}");
|
||||
DiagnosticLog.Debug("LinuxDispatcher", $"Initialized on thread {_mainThreadId}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ public class LinuxDispatcher : IDispatcher
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[LinuxDispatcher] Error in dispatched action: " + ex.Message);
|
||||
DiagnosticLog.Error("LinuxDispatcher", "Error in dispatched action", ex);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -62,7 +63,7 @@ public class LinuxDispatcher : IDispatcher
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[LinuxDispatcher] Error in delayed action: " + ex.Message);
|
||||
DiagnosticLog.Error("LinuxDispatcher", "Error in delayed action", ex);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Microsoft.Maui.Dispatching;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Dispatching;
|
||||
|
||||
@@ -95,7 +96,7 @@ public class LinuxDispatcherTimer : IDispatcherTimer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[LinuxDispatcherTimer] Error in Tick handler: " + ex.Message);
|
||||
DiagnosticLog.Error("LinuxDispatcherTimer", "Error in Tick handler", ex);
|
||||
}
|
||||
if (_isRepeating && _isRunning)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -111,13 +112,13 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("BorderHandler", $"Adding content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.AddChild(skiaContent);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +157,7 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
if (border is VisualElement ve)
|
||||
{
|
||||
var bgColor = ve.BackgroundColor;
|
||||
Console.WriteLine($"[BorderHandler] MapBackgroundColor: {bgColor}");
|
||||
DiagnosticLog.Debug("BorderHandler", $"MapBackgroundColor: {bgColor}");
|
||||
if (bgColor != null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = bgColor;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -64,7 +65,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
// 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}");
|
||||
DiagnosticLog.Debug("ButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
|
||||
if (mauiButton.WidthRequest >= 0)
|
||||
platformView.WidthRequest = mauiButton.WidthRequest;
|
||||
if (mauiButton.HeightRequest >= 0)
|
||||
@@ -72,7 +73,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[ButtonHandler] VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
|
||||
DiagnosticLog.Debug("ButtonHandler", $"VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,7 @@ public partial class TextButtonHandler : ButtonHandler
|
||||
|
||||
protected override void ConnectHandler(SkiaButton platformView)
|
||||
{
|
||||
Console.WriteLine($"[TextButtonHandler] ConnectHandler START");
|
||||
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler START");
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Manually map text properties on connect since MAUI may not trigger updates
|
||||
@@ -178,13 +179,13 @@ public partial class TextButtonHandler : ButtonHandler
|
||||
// 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}");
|
||||
DiagnosticLog.Debug("TextButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
|
||||
if (mauiButton.WidthRequest >= 0)
|
||||
platformView.WidthRequest = mauiButton.WidthRequest;
|
||||
if (mauiButton.HeightRequest >= 0)
|
||||
platformView.HeightRequest = mauiButton.HeightRequest;
|
||||
}
|
||||
Console.WriteLine($"[TextButtonHandler] ConnectHandler DONE");
|
||||
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler DONE");
|
||||
}
|
||||
|
||||
public static void MapText(TextButtonHandler handler, ITextButton button)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -130,18 +131,18 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
{
|
||||
_isUpdatingSelection = true;
|
||||
|
||||
Console.WriteLine($"[CollectionViewHandler] OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}");
|
||||
DiagnosticLog.Debug("CollectionViewHandler", $"OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}");
|
||||
|
||||
// Try to get the item view and process gestures
|
||||
var skiaView = PlatformView?.GetItemView(e.Index);
|
||||
Console.WriteLine($"[CollectionViewHandler] GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? "null"}, MauiView={skiaView?.MauiView?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("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");
|
||||
DiagnosticLog.Debug("CollectionViewHandler", "Gesture processed successfully");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -208,7 +209,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
{
|
||||
// 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}");
|
||||
DiagnosticLog.Debug("CollectionViewHandler", $"ItemViewCreator: Set MauiView={view.GetType().Name} on {skiaView.GetType().Name}, GestureRecognizers={view.GestureRecognizers?.Count ?? 0}");
|
||||
return skiaView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +70,23 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private bool _isUpdatingText;
|
||||
|
||||
private void OnTextChanged(object? sender, Platform.TextChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is null || PlatformView is null) return;
|
||||
if (VirtualView is null || PlatformView is null || _isUpdatingText) return;
|
||||
|
||||
if (VirtualView.Text != e.NewTextValue)
|
||||
{
|
||||
VirtualView.Text = e.NewTextValue ?? string.Empty;
|
||||
_isUpdatingText = true;
|
||||
try
|
||||
{
|
||||
VirtualView.Text = e.NewTextValue ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingText = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +97,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
|
||||
public static void MapText(EntryHandler handler, IEntry entry)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
if (handler.PlatformView is null || handler._isUpdatingText) return;
|
||||
|
||||
if (handler.PlatformView.Text != entry.Text)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -13,6 +14,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
/// </summary>
|
||||
public static class GestureManager
|
||||
{
|
||||
private const string Tag = "GestureManager";
|
||||
|
||||
private class GestureTrackingState
|
||||
{
|
||||
public double StartX { get; set; }
|
||||
@@ -35,16 +38,54 @@ public static class GestureManager
|
||||
Released
|
||||
}
|
||||
|
||||
// Cached reflection MethodInfo for internal MAUI methods
|
||||
private static MethodInfo? _sendTappedMethod;
|
||||
private static MethodInfo? _sendSwipedMethod;
|
||||
private static MethodInfo? _sendPanMethod;
|
||||
private static MethodInfo? _sendPinchMethod;
|
||||
private static 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 static MethodInfo? _sendDragStartingMethod;
|
||||
private static MethodInfo? _sendDragOverMethod;
|
||||
private static MethodInfo? _sendDropMethod;
|
||||
private static readonly Dictionary<PointerEventType, MethodInfo?> _pointerMethodCache = new();
|
||||
|
||||
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
|
||||
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new();
|
||||
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new();
|
||||
|
||||
/// <summary>
|
||||
/// Minimum distance in pixels for a swipe gesture to be recognized.
|
||||
/// </summary>
|
||||
public static double SwipeMinDistance { get; set; } = 50.0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in milliseconds for a swipe gesture to be recognized.
|
||||
/// </summary>
|
||||
public static double SwipeMaxTime { get; set; } = 500.0;
|
||||
|
||||
/// <summary>
|
||||
/// Ratio threshold for determining swipe direction dominance.
|
||||
/// </summary>
|
||||
public static double SwipeDirectionThreshold { get; set; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum distance in pixels before a pan gesture is recognized.
|
||||
/// </summary>
|
||||
public static double PanMinDistance { get; set; } = 10.0;
|
||||
|
||||
/// <summary>
|
||||
/// Scale factor per scroll unit for pinch-via-scroll gestures.
|
||||
/// </summary>
|
||||
public static double PinchScrollScale { get; set; } = 0.1;
|
||||
|
||||
/// <summary>
|
||||
/// Removes tracking entries for the specified view, preventing memory leaks
|
||||
/// when views are disconnected from the visual tree.
|
||||
/// </summary>
|
||||
public static void CleanupView(View view)
|
||||
{
|
||||
if (view == null) return;
|
||||
_tapTracking.Remove(view);
|
||||
_gestureState.Remove(view);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a tap gesture on the specified view.
|
||||
@@ -79,12 +120,13 @@ public static class GestureManager
|
||||
bool result = false;
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var tapRecognizer = (item is TapGestureRecognizer) ? (TapGestureRecognizer)item : null;
|
||||
if (tapRecognizer == null)
|
||||
if (item is not TapGestureRecognizer tapRecognizer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine($"[GestureManager] Processing TapGestureRecognizer on {view.GetType().Name}, CommandParameter={tapRecognizer.CommandParameter}, NumberOfTapsRequired={tapRecognizer.NumberOfTapsRequired}");
|
||||
DiagnosticLog.Debug(Tag,
|
||||
$"Processing TapGestureRecognizer on {view.GetType().Name}, CommandParameter={tapRecognizer.CommandParameter}, NumberOfTapsRequired={tapRecognizer.NumberOfTapsRequired}");
|
||||
|
||||
int numberOfTapsRequired = tapRecognizer.NumberOfTapsRequired;
|
||||
if (numberOfTapsRequired > 1)
|
||||
{
|
||||
@@ -92,108 +134,58 @@ public static class GestureManager
|
||||
if (!_tapTracking.TryGetValue(view, out var tracking))
|
||||
{
|
||||
_tapTracking[view] = (utcNow, 1);
|
||||
Console.WriteLine($"[GestureManager] First tap 1/{numberOfTapsRequired}");
|
||||
DiagnosticLog.Debug(Tag, $"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}");
|
||||
DiagnosticLog.Debug(Tag, $"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");
|
||||
DiagnosticLog.Debug(Tag, $"Tap {tapCount}/{numberOfTapsRequired}, waiting for more taps");
|
||||
continue;
|
||||
}
|
||||
_tapTracking.Remove(view);
|
||||
}
|
||||
|
||||
// Try to raise the Tapped event via cached reflection
|
||||
bool eventFired = false;
|
||||
try
|
||||
{
|
||||
if (_sendTappedMethod == null)
|
||||
{
|
||||
_sendTappedMethod = typeof(TapGestureRecognizer).GetMethod("SendTapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
_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");
|
||||
DiagnosticLog.Debug(Tag, "SendTapped invoked successfully");
|
||||
eventFired = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GestureManager] SendTapped failed: " + ex.Message);
|
||||
DiagnosticLog.Error(Tag, "SendTapped failed", ex);
|
||||
}
|
||||
|
||||
// Always invoke the Command if available (SendTapped may or may not invoke it internally)
|
||||
if (!eventFired)
|
||||
{
|
||||
try
|
||||
ICommand? command = tapRecognizer.Command;
|
||||
if (command != null && command.CanExecute(tapRecognizer.CommandParameter))
|
||||
{
|
||||
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);
|
||||
DiagnosticLog.Debug(Tag, "Executing TapGestureRecognizer Command");
|
||||
command.Execute(tapRecognizer.CommandParameter);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -274,7 +266,7 @@ public static class GestureManager
|
||||
}
|
||||
double deltaX = x - state.StartX;
|
||||
double deltaY = y - state.StartY;
|
||||
if (Math.Sqrt(deltaX * deltaX + deltaY * deltaY) >= 10.0)
|
||||
if (Math.Sqrt(deltaX * deltaX + deltaY * deltaY) >= PanMinDistance)
|
||||
{
|
||||
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)(state.IsPanning ? 1 : 0));
|
||||
state.IsPanning = true;
|
||||
@@ -299,14 +291,14 @@ public static class GestureManager
|
||||
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)
|
||||
if (distance >= SwipeMinDistance && elapsed <= SwipeMaxTime)
|
||||
{
|
||||
var direction = DetermineSwipeDirection(deltaX, deltaY);
|
||||
if (direction != SwipeDirection.Right)
|
||||
{
|
||||
ProcessSwipeGesture(view, direction);
|
||||
}
|
||||
else if (Math.Abs(deltaX) > Math.Abs(deltaY) * 0.5)
|
||||
else if (Math.Abs(deltaX) > Math.Abs(deltaY) * SwipeDirectionThreshold)
|
||||
{
|
||||
ProcessSwipeGesture(view, (deltaX > 0.0) ? SwipeDirection.Right : SwipeDirection.Left);
|
||||
}
|
||||
@@ -315,9 +307,9 @@ public static class GestureManager
|
||||
{
|
||||
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)2);
|
||||
}
|
||||
else if (distance < 15.0 && elapsed < 500.0)
|
||||
else if (distance < 15.0 && elapsed < SwipeMaxTime)
|
||||
{
|
||||
Console.WriteLine($"[GestureManager] Detected tap on {view.GetType().Name} (distance={distance:F1}, elapsed={elapsed:F0}ms)");
|
||||
DiagnosticLog.Debug(Tag, $"Detected tap on {view.GetType().Name} (distance={distance:F1}, elapsed={elapsed:F0}ms)");
|
||||
ProcessTap(view, x, y);
|
||||
}
|
||||
_gestureState.Remove(view);
|
||||
@@ -351,7 +343,7 @@ public static class GestureManager
|
||||
{
|
||||
double absX = Math.Abs(deltaX);
|
||||
double absY = Math.Abs(deltaY);
|
||||
if (absX > absY * 0.5)
|
||||
if (absX > absY * SwipeDirectionThreshold)
|
||||
{
|
||||
if (deltaX > 0.0)
|
||||
{
|
||||
@@ -359,7 +351,7 @@ public static class GestureManager
|
||||
}
|
||||
return SwipeDirection.Left;
|
||||
}
|
||||
if (absY > absX * 0.5)
|
||||
if (absY > absX * SwipeDirectionThreshold)
|
||||
{
|
||||
if (deltaY > 0.0)
|
||||
{
|
||||
@@ -383,29 +375,34 @@ public static class GestureManager
|
||||
}
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var swipeRecognizer = (item is SwipeGestureRecognizer) ? (SwipeGestureRecognizer)item : null;
|
||||
if (swipeRecognizer == null || !swipeRecognizer.Direction.HasFlag(direction))
|
||||
if (item is not SwipeGestureRecognizer swipeRecognizer || !swipeRecognizer.Direction.HasFlag(direction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine($"[GestureManager] Swipe detected: {direction}");
|
||||
DiagnosticLog.Debug(Tag, $"Swipe detected: {direction}");
|
||||
|
||||
try
|
||||
{
|
||||
var method = typeof(SwipeGestureRecognizer).GetMethod("SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (method != null)
|
||||
if (_sendSwipedMethod == null)
|
||||
{
|
||||
method.Invoke(swipeRecognizer, new object[] { view, direction });
|
||||
Console.WriteLine("[GestureManager] SendSwiped invoked successfully");
|
||||
_sendSwipedMethod = typeof(SwipeGestureRecognizer).GetMethod(
|
||||
"SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
if (_sendSwipedMethod != null)
|
||||
{
|
||||
_sendSwipedMethod.Invoke(swipeRecognizer, new object[] { view, direction });
|
||||
DiagnosticLog.Debug(Tag, "SendSwiped invoked successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GestureManager] SendSwiped failed: " + ex.Message);
|
||||
DiagnosticLog.Error(Tag, "SendSwiped failed", ex);
|
||||
}
|
||||
|
||||
ICommand? command = swipeRecognizer.Command;
|
||||
if (command != null && command.CanExecute(swipeRecognizer.CommandParameter))
|
||||
{
|
||||
swipeRecognizer.Command.Execute(swipeRecognizer.CommandParameter);
|
||||
command.Execute(swipeRecognizer.CommandParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,18 +416,22 @@ public static class GestureManager
|
||||
}
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var panRecognizer = (item is PanGestureRecognizer) ? (PanGestureRecognizer)item : null;
|
||||
if (panRecognizer == null)
|
||||
if (item is not PanGestureRecognizer panRecognizer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine($"[GestureManager] Pan gesture: status={status}, totalX={totalX:F1}, totalY={totalY:F1}");
|
||||
DiagnosticLog.Debug(Tag, $"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)
|
||||
if (_sendPanMethod == null)
|
||||
{
|
||||
method.Invoke(panRecognizer, new object[]
|
||||
_sendPanMethod = typeof(PanGestureRecognizer).GetMethod(
|
||||
"SendPan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
if (_sendPanMethod != null)
|
||||
{
|
||||
_sendPanMethod.Invoke(panRecognizer, new object[]
|
||||
{
|
||||
view,
|
||||
totalX,
|
||||
@@ -441,7 +442,7 @@ public static class GestureManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GestureManager] SendPan failed: " + ex.Message);
|
||||
DiagnosticLog.Error(Tag, "SendPan failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,8 +456,7 @@ public static class GestureManager
|
||||
}
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var pointerRecognizer = (item is PointerGestureRecognizer) ? (PointerGestureRecognizer)item : null;
|
||||
if (pointerRecognizer == null)
|
||||
if (item is not PointerGestureRecognizer pointerRecognizer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -471,19 +471,27 @@ public static class GestureManager
|
||||
PointerEventType.Released => "SendPointerReleased",
|
||||
_ => null,
|
||||
};
|
||||
if (methodName != 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 });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_pointerMethodCache.TryGetValue(eventType, out var method))
|
||||
{
|
||||
method = typeof(PointerGestureRecognizer).GetMethod(
|
||||
methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
_pointerMethodCache[eventType] = method;
|
||||
}
|
||||
|
||||
if (method != null)
|
||||
{
|
||||
var args = CreatePointerEventArgs(view, x, y);
|
||||
method.Invoke(pointerRecognizer, new object[] { view, args });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GestureManager] Pointer event failed: " + ex.Message);
|
||||
DiagnosticLog.Error(Tag, $"Pointer event {eventType} failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,8 +510,9 @@ public static class GestureManager
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Debug("GestureManager", "PointerEventArgs creation failed", ex);
|
||||
}
|
||||
return null!;
|
||||
}
|
||||
@@ -587,26 +596,23 @@ public static class GestureManager
|
||||
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var pinchRecognizer = item as PinchGestureRecognizer;
|
||||
if (pinchRecognizer == null)
|
||||
if (item is not PinchGestureRecognizer pinchRecognizer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GestureManager] Pinch gesture: status={status}, scale={scale:F2}, origin=({originX:F0},{originY:F0})");
|
||||
DiagnosticLog.Debug(Tag, $"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);
|
||||
_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[]
|
||||
{
|
||||
@@ -615,12 +621,12 @@ public static class GestureManager
|
||||
scaleOrigin,
|
||||
status
|
||||
});
|
||||
Console.WriteLine("[GestureManager] SendPinch invoked successfully");
|
||||
DiagnosticLog.Debug(Tag, "SendPinch invoked successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GestureManager] SendPinch failed: {ex.Message}");
|
||||
DiagnosticLog.Error(Tag, "SendPinch failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -751,26 +757,27 @@ public static class GestureManager
|
||||
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var dragRecognizer = item as DragGestureRecognizer;
|
||||
if (dragRecognizer == null) continue;
|
||||
if (item is not DragGestureRecognizer dragRecognizer) continue;
|
||||
|
||||
Console.WriteLine($"[GestureManager] Starting drag from {view.GetType().Name}");
|
||||
DiagnosticLog.Debug(Tag, $"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)
|
||||
if (_sendDragStartingMethod == null)
|
||||
{
|
||||
method.Invoke(dragRecognizer, new object[] { view });
|
||||
Console.WriteLine("[GestureManager] SendDragStarting invoked successfully");
|
||||
_sendDragStartingMethod = typeof(DragGestureRecognizer).GetMethod(
|
||||
"SendDragStarting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (_sendDragStartingMethod != null)
|
||||
{
|
||||
_sendDragStartingMethod.Invoke(dragRecognizer, new object[] { view });
|
||||
DiagnosticLog.Debug(Tag, "SendDragStarting invoked successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GestureManager] SendDragStarting failed: {ex.Message}");
|
||||
DiagnosticLog.Error(Tag, "SendDragStarting failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,24 +794,26 @@ public static class GestureManager
|
||||
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var dropRecognizer = item as DropGestureRecognizer;
|
||||
if (dropRecognizer == null) continue;
|
||||
if (item is not DropGestureRecognizer dropRecognizer) continue;
|
||||
|
||||
Console.WriteLine($"[GestureManager] Drag enter on {view.GetType().Name}");
|
||||
DiagnosticLog.Debug(Tag, $"Drag enter on {view.GetType().Name}");
|
||||
|
||||
try
|
||||
{
|
||||
var method = typeof(DropGestureRecognizer).GetMethod("SendDragOver",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
if (method != null)
|
||||
if (_sendDragOverMethod == null)
|
||||
{
|
||||
method.Invoke(dropRecognizer, new object[] { view });
|
||||
_sendDragOverMethod = typeof(DropGestureRecognizer).GetMethod(
|
||||
"SendDragOver", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (_sendDragOverMethod != null)
|
||||
{
|
||||
_sendDragOverMethod.Invoke(dropRecognizer, new object[] { view });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GestureManager] SendDragOver failed: {ex.Message}");
|
||||
DiagnosticLog.Error(Tag, "SendDragOver failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -821,24 +830,26 @@ public static class GestureManager
|
||||
|
||||
foreach (var item in recognizers)
|
||||
{
|
||||
var dropRecognizer = item as DropGestureRecognizer;
|
||||
if (dropRecognizer == null) continue;
|
||||
if (item is not DropGestureRecognizer dropRecognizer) continue;
|
||||
|
||||
Console.WriteLine($"[GestureManager] Drop on {view.GetType().Name}");
|
||||
DiagnosticLog.Debug(Tag, $"Drop on {view.GetType().Name}");
|
||||
|
||||
try
|
||||
{
|
||||
var method = typeof(DropGestureRecognizer).GetMethod("SendDrop",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
if (method != null)
|
||||
if (_sendDropMethod == null)
|
||||
{
|
||||
method.Invoke(dropRecognizer, new object[] { view });
|
||||
_sendDropMethod = typeof(DropGestureRecognizer).GetMethod(
|
||||
"SendDrop", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
if (_sendDropMethod != null)
|
||||
{
|
||||
_sendDropMethod.Invoke(dropRecognizer, new object[] { view });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GestureManager] SendDrop failed: {ex.Message}");
|
||||
DiagnosticLog.Error(Tag, "SendDrop failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
_platformWebView.NavigationCompleted += OnNavigationCompleted;
|
||||
_platformWebView.ScriptDialogRequested += OnScriptDialogRequested;
|
||||
}
|
||||
Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", "ConnectHandler - WebView ready");
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(GtkWebViewProxy platformView)
|
||||
@@ -75,7 +75,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
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}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Script dialog requested: type={e.Type}, message={e.Message}");
|
||||
|
||||
string title = e.Type switch
|
||||
{
|
||||
@@ -92,18 +92,18 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
{
|
||||
bool result = await LinuxDialogService.ShowAlertAsync(title, e.Message, acceptButton, cancelButton);
|
||||
e.Callback(result);
|
||||
Console.WriteLine($"[GtkWebViewHandler] Dialog result: {result}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Dialog result: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Error showing dialog: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewHandler", $"Error showing dialog: {ex.Message}", ex);
|
||||
e.Callback(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNavigationStarted(object? sender, string uri)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Navigation started: {uri}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Navigation started: {uri}");
|
||||
try
|
||||
{
|
||||
GLibNative.IdleAdd(() =>
|
||||
@@ -115,25 +115,25 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
var args = new Microsoft.Maui.Controls.WebNavigatingEventArgs(
|
||||
WebNavigationEvent.NewPage, null, uri);
|
||||
controller.SendNavigating(args);
|
||||
Console.WriteLine("[GtkWebViewHandler] Sent Navigating event to VirtualView");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", "Sent Navigating event to VirtualView");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigating: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewHandler", $"Error in SendNavigating: {ex.Message}", ex);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation started: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewHandler", $"Error dispatching navigation started: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNavigationCompleted(object? sender, (string Url, bool Success) e)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Navigation completed: {e.Url} (Success: {e.Success})");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Navigation completed: {e.Url} (Success: {e.Success})");
|
||||
try
|
||||
{
|
||||
GLibNative.IdleAdd(() =>
|
||||
@@ -151,19 +151,19 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
bool canGoForward = _platformWebView?.CanGoForward() ?? false;
|
||||
controller.CanGoBack = canGoBack;
|
||||
controller.CanGoForward = canGoForward;
|
||||
Console.WriteLine($"[GtkWebViewHandler] Sent Navigated, CanGoBack={canGoBack}, CanGoForward={canGoForward}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Sent Navigated, CanGoBack={canGoBack}, CanGoForward={canGoForward}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigated: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewHandler", $"Error in SendNavigated: {ex.Message}", ex);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation completed: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewHandler", $"Error dispatching navigation completed: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
var hostService = GtkHostService.Instance;
|
||||
if (hostService.HostWindow == null || hostService.WebViewManager == null)
|
||||
{
|
||||
Console.WriteLine("[GtkWebViewHandler] Warning: GTK host not initialized, cannot register WebView");
|
||||
DiagnosticLog.Warn("GtkWebViewHandler", "GTK host not initialized, cannot register WebView");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] Skipping invalid bounds: {bounds}");
|
||||
DiagnosticLog.Warn("GtkWebViewHandler", $"Skipping invalid bounds: {bounds}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,12 +194,12 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
{
|
||||
hostService.HostWindow.AddWebView(_platformWebView.Widget, x, y, width, height);
|
||||
_isRegisteredWithHost = true;
|
||||
Console.WriteLine($"[GtkWebViewHandler] Registered WebView at ({x}, {y}) size {width}x{height}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Registered WebView at ({x}, {y}) size {width}x{height}");
|
||||
}
|
||||
else if (bounds != _lastBounds)
|
||||
{
|
||||
hostService.HostWindow.MoveResizeWebView(_platformWebView.Widget, x, y, width, height);
|
||||
Console.WriteLine($"[GtkWebViewHandler] Updated WebView to ({x}, {y}) size {width}x{height}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"Updated WebView to ({x}, {y}) size {width}x{height}");
|
||||
}
|
||||
|
||||
_lastBounds = bounds;
|
||||
@@ -213,7 +213,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
if (hostService.HostWindow != null)
|
||||
{
|
||||
hostService.HostWindow.RemoveWebView(_platformWebView.Widget);
|
||||
Console.WriteLine("[GtkWebViewHandler] Unregistered WebView from host");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", "Unregistered WebView from host");
|
||||
}
|
||||
_isRegisteredWithHost = false;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
return;
|
||||
|
||||
var source = webView.Source;
|
||||
Console.WriteLine($"[GtkWebViewHandler] MapSource: {source?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", $"MapSource: {source?.GetType().Name ?? "null"}");
|
||||
|
||||
if (source is UrlWebViewSource urlSource)
|
||||
{
|
||||
@@ -247,19 +247,19 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
||||
|
||||
public static void MapGoBack(GtkWebViewHandler handler, IWebView webView, object? args)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewHandler] MapGoBack called, CanGoBack={handler._platformWebView?.CanGoBack()}");
|
||||
DiagnosticLog.Debug("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()}");
|
||||
DiagnosticLog.Debug("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");
|
||||
DiagnosticLog.Debug("GtkWebViewHandler", "MapReload called");
|
||||
handler._platformWebView?.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged += _themeChangedHandler;
|
||||
}
|
||||
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Created WebKitWebView widget");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,7 +105,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
var dialogType = (ScriptDialogType)(int)webkitDialogType;
|
||||
var message = WebKitNative.GetScriptDialogMessage(dialog) ?? "";
|
||||
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Script dialog: type={dialogType}, message={message}");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Script dialog: type={dialogType}, message={message}");
|
||||
|
||||
// Get the parent window for proper modal behavior
|
||||
IntPtr parentWindow = GtkHostService.Instance.HostWindow?.Window ?? IntPtr.Zero;
|
||||
@@ -166,7 +166,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
|
||||
// Run the dialog synchronously - this blocks until user responds
|
||||
int response = GtkNative.gtk_dialog_run(gtkDialog);
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Dialog response: {response}");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Dialog response: {response}");
|
||||
|
||||
// Set the confirmed state for confirm dialogs
|
||||
if (dialogType == ScriptDialogType.Confirm || dialogType == ScriptDialogType.BeforeUnloadConfirm)
|
||||
@@ -184,7 +184,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Error in OnScriptDialog: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error in OnScriptDialog: {ex.Message}", ex);
|
||||
// Return false on error to let WebKitGTK try its default handling
|
||||
return false;
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
|
||||
if (gtkDialog == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Failed to create prompt dialog");
|
||||
DiagnosticLog.Error("GtkWebViewPlatformView", "Failed to create prompt dialog");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
|
||||
// Run the dialog
|
||||
int response = GtkNative.gtk_dialog_run(gtkDialog);
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Prompt dialog response: {response}");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Prompt dialog response: {response}");
|
||||
|
||||
if (response == GtkNative.GTK_RESPONSE_OK)
|
||||
{
|
||||
@@ -261,7 +261,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
? System.Runtime.InteropServices.Marshal.PtrToStringUTF8(textPtr)
|
||||
: "";
|
||||
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Prompt text: {enteredText}");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", $"Prompt text: {enteredText}");
|
||||
|
||||
// Set the prompt response
|
||||
WebKitNative.SetScriptDialogPromptText(webkitDialog, enteredText ?? "");
|
||||
@@ -278,7 +278,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Error in HandlePromptDialog: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error in HandlePromptDialog: {ex.Message}", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
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}");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", $"ApplyDialogTheme: isDark={isDark}, UserAppTheme={Microsoft.Maui.Controls.Application.Current?.UserAppTheme}");
|
||||
|
||||
// Create comprehensive CSS based on the theme - targeting all dialog elements
|
||||
string css = isDark
|
||||
@@ -407,7 +407,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkWebViewPlatformView] Error applying dialog theme: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkWebViewPlatformView", $"Error applying dialog theme: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,17 +419,17 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
switch ((WebKitNative.WebKitLoadEvent)loadEvent)
|
||||
{
|
||||
case WebKitNative.WebKitLoadEvent.Started:
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Load started: " + uri);
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load started: " + uri);
|
||||
NavigationStarted?.Invoke(this, uri);
|
||||
break;
|
||||
case WebKitNative.WebKitLoadEvent.Finished:
|
||||
_currentUri = uri;
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Load finished: " + uri);
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load finished: " + uri);
|
||||
NavigationCompleted?.Invoke(this, (uri, true));
|
||||
break;
|
||||
case WebKitNative.WebKitLoadEvent.Committed:
|
||||
_currentUri = uri;
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Load committed: " + uri);
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load committed: " + uri);
|
||||
break;
|
||||
case WebKitNative.WebKitLoadEvent.Redirected:
|
||||
break;
|
||||
@@ -437,8 +437,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Error in OnLoadChanged: " + ex.Message);
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Stack trace: " + ex.StackTrace);
|
||||
DiagnosticLog.Error("GtkWebViewPlatformView", "Error in OnLoadChanged: " + ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +446,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
if (_widget != IntPtr.Zero)
|
||||
{
|
||||
WebKitNative.LoadUri(_widget, uri);
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Navigate to: " + uri);
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Navigate to: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +455,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
||||
if (_widget != IntPtr.Zero)
|
||||
{
|
||||
WebKitNative.LoadHtml(_widget, html, baseUri);
|
||||
Console.WriteLine("[GtkWebViewPlatformView] Load HTML content");
|
||||
DiagnosticLog.Debug("GtkWebViewPlatformView", "Load HTML content");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -127,7 +128,7 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
if (image is Image img && img.WidthRequest > 0)
|
||||
{
|
||||
handler.PlatformView.WidthRequest = img.WidthRequest;
|
||||
Console.WriteLine($"[ImageHandler] MapWidth: {img.WidthRequest}");
|
||||
DiagnosticLog.Debug("ImageHandler", $"MapWidth: {img.WidthRequest}");
|
||||
}
|
||||
else if (image.Width > 0)
|
||||
{
|
||||
@@ -142,7 +143,7 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
if (image is Image img && img.HeightRequest > 0)
|
||||
{
|
||||
handler.PlatformView.HeightRequest = img.HeightRequest;
|
||||
Console.WriteLine($"[ImageHandler] MapHeight: {img.HeightRequest}");
|
||||
DiagnosticLog.Debug("ImageHandler", $"MapHeight: {img.HeightRequest}");
|
||||
}
|
||||
else if (image.Height > 0)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -279,13 +280,13 @@ public partial class GridHandler : LayoutHandler
|
||||
|
||||
protected override void ConnectHandler(SkiaLayoutView platformView)
|
||||
{
|
||||
Console.WriteLine($"[GridHandler.ConnectHandler] Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
// Map definitions on connect
|
||||
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
|
||||
{
|
||||
Console.WriteLine($"[GridHandler.ConnectHandler] Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
|
||||
UpdateRowDefinitions(grid, gridLayout);
|
||||
UpdateColumnDefinitions(grid, gridLayout);
|
||||
|
||||
@@ -295,13 +296,13 @@ public partial class GridHandler : LayoutHandler
|
||||
var child = gridLayout[i];
|
||||
if (child == null) continue;
|
||||
|
||||
Console.WriteLine($"[GridHandler.ConnectHandler] Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
|
||||
}
|
||||
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
@@ -315,11 +316,11 @@ public partial class GridHandler : LayoutHandler
|
||||
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}");
|
||||
DiagnosticLog.Debug("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");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid now has {grid.Children.Count} SkiaView children");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -245,7 +246,7 @@ public partial class GridHandler : LayoutHandler
|
||||
// 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");
|
||||
DiagnosticLog.Debug("GridHandler", $"ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols");
|
||||
|
||||
// Explicitly map BackgroundColor since it may be set before handler creation
|
||||
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
|
||||
@@ -258,7 +259,7 @@ public partial class GridHandler : LayoutHandler
|
||||
{
|
||||
var padding = paddable.Padding;
|
||||
platformView.Padding = padding;
|
||||
Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
|
||||
DiagnosticLog.Debug("GridHandler", $"Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
|
||||
}
|
||||
|
||||
// Map row/column definitions first
|
||||
@@ -271,7 +272,7 @@ public partial class GridHandler : LayoutHandler
|
||||
var child = gridLayout[i];
|
||||
if (child == null) continue;
|
||||
|
||||
Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}");
|
||||
DiagnosticLog.Debug("GridHandler", $"Processing child {i}: {child.GetType().Name}");
|
||||
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
@@ -289,21 +290,20 @@ public partial class GridHandler : LayoutHandler
|
||||
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
|
||||
DiagnosticLog.Debug("GridHandler", $"Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
|
||||
|
||||
// Add child's platform view to our grid
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
{
|
||||
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
|
||||
Console.WriteLine($"[GridHandler] Added child {i} to grid");
|
||||
DiagnosticLog.Debug("GridHandler", $"Added child {i} to grid");
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"[GridHandler] ConnectHandler complete");
|
||||
DiagnosticLog.Debug("GridHandler", "ConnectHandler complete");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}");
|
||||
DiagnosticLog.Error("GridHandler", $"EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Svg.Skia;
|
||||
using System.Collections.Specialized;
|
||||
@@ -88,12 +89,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
// Get all pages in the navigation stack
|
||||
var pages = VirtualView.Navigation.NavigationStack.ToList();
|
||||
Console.WriteLine($"[NavigationPageHandler] Setting up {pages.Count} pages");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
|
||||
pages.Add(VirtualView.CurrentPage);
|
||||
}
|
||||
|
||||
@@ -102,12 +103,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
// Ensure the page has a handler
|
||||
if (page.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Page handler type: {page.Handler?.GetType().Name}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
|
||||
|
||||
if (page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
{
|
||||
@@ -117,12 +118,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||
skiaPage.Title = page.Title ?? "";
|
||||
|
||||
Console.WriteLine($"[NavigationPageHandler] SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||
|
||||
// If content is null, try to get it from ContentPage
|
||||
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
|
||||
if (contentPage.Content.Handler == null)
|
||||
{
|
||||
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
|
||||
@@ -130,7 +131,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
skiaPage.Content = skiaContent;
|
||||
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,18 +140,18 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
|
||||
if (PlatformView.StackDepth == 0)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Setting root page: {page.Title}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Setting root page: {page.Title}");
|
||||
PlatformView.SetRootPage(skiaPage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Pushing page: {page.Title}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Pushing page: {page.Title}");
|
||||
PlatformView.Push(skiaPage, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Failed to get SkiaPage for: {page.Title}");
|
||||
DiagnosticLog.Warn("NavigationPageHandler", $"Failed to get SkiaPage for: {page.Title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,12 +162,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
{
|
||||
if (skiaPage is SkiaContentPage contentPage)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Adding toolbar item: '{item.Text}', IconImageSource={item.IconImageSource}, Order={item.Order}");
|
||||
// Default and Primary should both be treated as Primary (shown in toolbar)
|
||||
// Only Secondary goes to overflow menu
|
||||
var order = item.Order == ToolbarItemOrder.Secondary
|
||||
@@ -177,7 +178,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
var toolbarItem = item; // Capture for closure
|
||||
var clickCommand = new RelayCommand(() =>
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
|
||||
// Use IMenuItemController to send the click
|
||||
if (toolbarItem is IMenuItemController menuController)
|
||||
{
|
||||
@@ -209,10 +210,10 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
// 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}'");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Subscribing to ToolbarItems changes for '{page.Title}'");
|
||||
notifyCollection.CollectionChanged += (s, e) =>
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItems changed for '{page.Title}', action={e.Action}");
|
||||
MapToolbarItems(skiaPage, page);
|
||||
skiaPage.Invalidate();
|
||||
};
|
||||
@@ -229,9 +230,9 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
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)})");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"LoadToolbarIcon: Looking for {fileName}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $" Trying PNG: {pngPath} (exists: {File.Exists(pngPath)})");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $" Trying SVG: {svgPath} (exists: {File.Exists(svgPath)})");
|
||||
|
||||
// Try SVG first
|
||||
if (File.Exists(svgPath))
|
||||
@@ -247,7 +248,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
canvas.Scale(scale);
|
||||
canvas.DrawPicture(svg.Picture, null);
|
||||
Console.WriteLine($"[NavigationPageHandler] Loaded SVG icon: {svgPath}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded SVG icon: {svgPath}");
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -257,16 +258,16 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
{
|
||||
using var stream = File.OpenRead(pngPath);
|
||||
var result = SKBitmap.Decode(stream);
|
||||
Console.WriteLine($"[NavigationPageHandler] Loaded PNG icon: {pngPath}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded PNG icon: {pngPath}");
|
||||
return result;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[NavigationPageHandler] Icon not found: {fileName}");
|
||||
DiagnosticLog.Warn("NavigationPageHandler", $"Icon not found: {fileName}");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Error loading icon {fileName}: {ex.Message}");
|
||||
DiagnosticLog.Error("NavigationPageHandler", $"Error loading icon {fileName}: {ex.Message}", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -275,20 +276,20 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Pushed: {e.Page?.Title}");
|
||||
if (e.Page == null || PlatformView == null || MauiContext == null) return;
|
||||
|
||||
// Ensure the page has a handler
|
||||
if (e.Page.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
|
||||
DiagnosticLog.Debug("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}");
|
||||
DiagnosticLog.Debug("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"}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
|
||||
skiaPage.ShowNavigationBar = true;
|
||||
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
|
||||
skiaPage.TitleTextColor = PlatformView.BarTextColor;
|
||||
@@ -297,7 +298,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
// 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}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, creating handler for: {contentPage.Content.GetType().Name}");
|
||||
if (contentPage.Content.Handler == null)
|
||||
{
|
||||
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
|
||||
@@ -305,36 +306,35 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
skiaPage.Content = skiaContent;
|
||||
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "Mapping toolbar items");
|
||||
MapToolbarItems(skiaPage, e.Page);
|
||||
Console.WriteLine($"[NavigationPageHandler] Pushing page to platform");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "Pushing page to platform");
|
||||
PlatformView.Push(skiaPage, false);
|
||||
Console.WriteLine($"[NavigationPageHandler] Push complete, thread={Environment.CurrentManagedThreadId}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"Push complete, thread={Environment.CurrentManagedThreadId}");
|
||||
}
|
||||
Console.WriteLine("[NavigationPageHandler] OnVirtualViewPushed returning");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "OnVirtualViewPushed returning");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}");
|
||||
DiagnosticLog.Error("NavigationPageHandler", $"EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Popped: {e.Page?.Title}");
|
||||
// Pop on the platform side to sync with MAUI navigation
|
||||
PlatformView?.Pop();
|
||||
}
|
||||
|
||||
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", "VirtualView PoppedToRoot");
|
||||
PlatformView?.PopToRoot();
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request)
|
||||
return;
|
||||
|
||||
Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {request.NavigationStack.Count} pages");
|
||||
DiagnosticLog.Debug("NavigationPageHandler", $"MapRequestNavigation: {request.NavigationStack.Count} pages");
|
||||
|
||||
// Handle navigation request
|
||||
foreach (var view in request.NavigationStack)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -67,7 +68,7 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
|
||||
private void OnAppearing(object? sender, EventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}");
|
||||
DiagnosticLog.Debug("PageHandler", $"OnAppearing received for: {VirtualView?.Title}");
|
||||
(VirtualView as IPageController)?.SendAppearing();
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
|
||||
if (backgroundColor != null && backgroundColor != Colors.Transparent)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = backgroundColor;
|
||||
Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}");
|
||||
DiagnosticLog.Debug("PageHandler", $"MapBackgroundColor: {backgroundColor}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,19 +190,19 @@ public partial class ContentPageHandler : PageHandler
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}");
|
||||
DiagnosticLog.Debug("ContentPageHandler", $"Creating handler for content: {content.GetType().Name}");
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
// The content's handler should provide the platform view
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}");
|
||||
DiagnosticLog.Debug("ContentPageHandler", $"Setting content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.Content = skiaContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Warn("ContentPageHandler", $"Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -235,7 +236,7 @@ public partial class ContentPageHandler : PageHandler
|
||||
if (item.IconImageSource is FileImageSource fileSource)
|
||||
{
|
||||
// Icon loading would be async - simplified for now
|
||||
Console.WriteLine($"[ContentPageHandler] Toolbar item icon: {fileSource.File}");
|
||||
DiagnosticLog.Debug("ContentPageHandler", $"Toolbar item icon: {fileSource.File}");
|
||||
}
|
||||
|
||||
platformView.ToolbarItems.Add(skiaItem);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -48,7 +49,7 @@ public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView
|
||||
var content = scrollView.PresentedContent;
|
||||
if (content != null)
|
||||
{
|
||||
Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}");
|
||||
DiagnosticLog.Debug("ScrollViewHandler", $"MapContent: {content.GetType().Name}");
|
||||
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
@@ -58,7 +59,7 @@ public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}");
|
||||
DiagnosticLog.Debug("ScrollViewHandler", $"Setting content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.Content = skiaContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -48,13 +49,13 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
|
||||
|
||||
protected override SkiaShell CreatePlatformView()
|
||||
{
|
||||
Console.WriteLine("[ShellHandler] CreatePlatformView - creating SkiaShell");
|
||||
DiagnosticLog.Debug("ShellHandler", "CreatePlatformView - creating SkiaShell");
|
||||
return new SkiaShell();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaShell platformView)
|
||||
{
|
||||
Console.WriteLine("[ShellHandler] ConnectHandler - connecting to SkiaShell");
|
||||
DiagnosticLog.Debug("ShellHandler", "ConnectHandler - connecting to SkiaShell");
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
||||
platformView.Navigated += OnNavigated;
|
||||
@@ -116,20 +117,20 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
|
||||
|
||||
private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}");
|
||||
DiagnosticLog.Debug("ShellHandler", $"Shell Navigating to: {e.Target?.Location}");
|
||||
|
||||
// Route to platform view
|
||||
if (PlatformView != null && e.Target?.Location != null)
|
||||
{
|
||||
var route = e.Target.Location.ToString().TrimStart('/');
|
||||
Console.WriteLine($"[ShellHandler] Routing to: {route}");
|
||||
DiagnosticLog.Debug("ShellHandler", $"Routing to: {route}");
|
||||
PlatformView.GoToAsync(route);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}");
|
||||
DiagnosticLog.Debug("ShellHandler", $"Shell Navigated to: {e.Current?.Location}");
|
||||
}
|
||||
|
||||
private void SyncShellItems()
|
||||
@@ -230,7 +231,7 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ShellHandler] Error rendering content: {ex.Message}");
|
||||
DiagnosticLog.Error("ShellHandler", $"Error rendering content: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
@@ -47,7 +48,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
|
||||
protected override LinuxWebView CreatePlatformView()
|
||||
{
|
||||
Console.WriteLine("[WebViewHandler] Creating LinuxWebView");
|
||||
DiagnosticLog.Debug("WebViewHandler", "Creating LinuxWebView");
|
||||
return new LinuxWebView();
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
MapUserAgent(this, VirtualView);
|
||||
}
|
||||
|
||||
Console.WriteLine("[WebViewHandler] Handler connected");
|
||||
DiagnosticLog.Debug("WebViewHandler", "Handler connected");
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(LinuxWebView platformView)
|
||||
@@ -74,7 +75,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
platformView.Navigated -= OnNavigated;
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
Console.WriteLine("[WebViewHandler] Handler disconnected");
|
||||
DiagnosticLog.Debug("WebViewHandler", "Handler disconnected");
|
||||
}
|
||||
|
||||
private void OnNavigating(object? sender, WebViewNavigatingEventArgs e)
|
||||
@@ -104,7 +105,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
Console.WriteLine($"[WebViewHandler] MapSource: {source.GetType().Name}");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"MapSource: {source.GetType().Name}");
|
||||
|
||||
if (source is IUrlWebViewSource urlSource && !string.IsNullOrEmpty(urlSource.Url))
|
||||
{
|
||||
@@ -121,7 +122,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent))
|
||||
{
|
||||
handler.PlatformView.UserAgent = webView.UserAgent;
|
||||
Console.WriteLine($"[WebViewHandler] MapUserAgent: {webView.UserAgent}");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"MapUserAgent: {webView.UserAgent}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +135,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
if (handler.PlatformView?.CanGoBack == true)
|
||||
{
|
||||
handler.PlatformView.GoBack();
|
||||
Console.WriteLine("[WebViewHandler] GoBack");
|
||||
DiagnosticLog.Debug("WebViewHandler", "GoBack");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,14 +144,14 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
if (handler.PlatformView?.CanGoForward == true)
|
||||
{
|
||||
handler.PlatformView.GoForward();
|
||||
Console.WriteLine("[WebViewHandler] GoForward");
|
||||
DiagnosticLog.Debug("WebViewHandler", "GoForward");
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
|
||||
{
|
||||
handler.PlatformView?.Reload();
|
||||
Console.WriteLine("[WebViewHandler] Reload");
|
||||
DiagnosticLog.Debug("WebViewHandler", "Reload");
|
||||
}
|
||||
|
||||
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
|
||||
@@ -158,7 +159,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
if (args is string script)
|
||||
{
|
||||
handler.PlatformView?.Eval(script);
|
||||
Console.WriteLine($"[WebViewHandler] Eval: {script.Substring(0, Math.Min(50, script.Length))}...");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"Eval: {script.Substring(0, Math.Min(50, script.Length))}...");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
|
||||
{
|
||||
request.SetResult(null);
|
||||
}
|
||||
Console.WriteLine($"[WebViewHandler] EvaluateJavaScriptAsync: {request.Script.Substring(0, Math.Min(50, request.Script.Length))}...");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"EvaluateJavaScriptAsync: {request.Script.Substring(0, Math.Min(50, request.Script.Length))}...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -89,30 +90,30 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
|
||||
|
||||
public static void MapSource(WebViewHandler handler, IWebView webView)
|
||||
{
|
||||
Console.WriteLine("[WebViewHandler] MapSource called");
|
||||
DiagnosticLog.Debug("WebViewHandler", "MapSource called");
|
||||
if (handler.PlatformView == null)
|
||||
{
|
||||
Console.WriteLine("[WebViewHandler] PlatformView is null!");
|
||||
DiagnosticLog.Warn("WebViewHandler", "PlatformView is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = webView.Source;
|
||||
Console.WriteLine($"[WebViewHandler] Source type: {source?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"Source type: {source?.GetType().Name ?? "null"}");
|
||||
|
||||
if (source is UrlWebViewSource urlSource)
|
||||
{
|
||||
Console.WriteLine($"[WebViewHandler] Loading URL: {urlSource.Url}");
|
||||
DiagnosticLog.Debug("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))}...");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"Loading HTML ({htmlSource.Html?.Length ?? 0} chars)");
|
||||
DiagnosticLog.Debug("WebViewHandler", $"HTML preview: {htmlSource.Html?.Substring(0, Math.Min(100, htmlSource.Html?.Length ?? 0))}...");
|
||||
handler.PlatformView.Html = htmlSource.Html ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[WebViewHandler] Unknown source type or null");
|
||||
DiagnosticLog.Debug("WebViewHandler", "Unknown source type or null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -81,19 +82,19 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
||||
|
||||
public static void MapContent(WindowHandler handler, IWindow window)
|
||||
{
|
||||
Console.Error.WriteLine($"[WindowHandler] MapContent - PlatformView={handler.PlatformView != null}");
|
||||
DiagnosticLog.Debug("WindowHandler", $"MapContent - PlatformView={handler.PlatformView != null}");
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
var content = window.Content;
|
||||
Console.Error.WriteLine($"[WindowHandler] MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
|
||||
DiagnosticLog.Debug("WindowHandler", $"MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
|
||||
if (content?.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
Console.Error.WriteLine($"[WindowHandler] MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
|
||||
DiagnosticLog.Debug("WindowHandler", $"MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
|
||||
handler.PlatformView.Content = skiaContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[WindowHandler] MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
|
||||
DiagnosticLog.Warn("WindowHandler", $"MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public static class LinuxProgramHost
|
||||
|
||||
// Initialize GTK for WebView support
|
||||
GtkHostService.Instance.Initialize(options.Title ?? "MAUI Application", options.Width, options.Height);
|
||||
Console.WriteLine("[LinuxProgramHost] GTK initialized for WebView support");
|
||||
DiagnosticLog.Debug("LinuxProgramHost", "GTK initialized for WebView support");
|
||||
|
||||
// Create Linux application
|
||||
using var linuxApp = new LinuxApplication();
|
||||
@@ -79,7 +79,7 @@ public static class LinuxProgramHost
|
||||
// Fallback to demo if no application view is available
|
||||
if (rootView == null)
|
||||
{
|
||||
Console.WriteLine("No application page found. Showing demo UI.");
|
||||
DiagnosticLog.Warn("LinuxProgramHost", "No application page found. Showing demo UI.");
|
||||
rootView = CreateDemoView();
|
||||
}
|
||||
|
||||
@@ -140,8 +140,8 @@ public static class LinuxProgramHost
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error rendering application: {ex.Message}");
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
DiagnosticLog.Error("LinuxProgramHost", $"Error rendering application: {ex.Message}");
|
||||
DiagnosticLog.Error("LinuxProgramHost", ex.StackTrace ?? "");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
@@ -38,13 +39,13 @@ public class LinuxViewRenderer
|
||||
{
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up the route - remove leading // or /
|
||||
var cleanRoute = route.TrimStart('/');
|
||||
Console.WriteLine($"[NavigateToRoute] Navigating to: {cleanRoute}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Navigating to: {cleanRoute}");
|
||||
|
||||
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
|
||||
{
|
||||
@@ -52,13 +53,13 @@ public class LinuxViewRenderer
|
||||
if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) ||
|
||||
section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Found section {i}: {section.Title}");
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[NavigateToRoute] Route not found: {cleanRoute}");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", $"NavigateToRoute: Route not found: {cleanRoute}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -74,17 +75,17 @@ public class LinuxViewRenderer
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool PushPage(Page page)
|
||||
{
|
||||
Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"PushPage: Pushing page: {page.GetType().Name}");
|
||||
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
Console.WriteLine($"[PushPage] CurrentSkiaShell is null");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CurrentRenderer == null)
|
||||
{
|
||||
Console.WriteLine($"[PushPage] CurrentRenderer is null");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentRenderer is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -96,18 +97,18 @@ public class LinuxViewRenderer
|
||||
|
||||
if (skiaPage == null)
|
||||
{
|
||||
Console.WriteLine($"[PushPage] Failed to render page through handler");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "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");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", "PushPage: Successfully pushed page via handler system");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[PushPage] Error: {ex.Message}");
|
||||
DiagnosticLog.Error("LinuxViewRenderer", "PushPage failed", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -118,11 +119,11 @@ public class LinuxViewRenderer
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool PopPage()
|
||||
{
|
||||
Console.WriteLine($"[PopPage] Popping page");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", "PopPage: Popping page");
|
||||
|
||||
if (CurrentSkiaShell == null)
|
||||
{
|
||||
Console.WriteLine($"[PopPage] CurrentSkiaShell is null");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "PopPage: CurrentSkiaShell is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -233,12 +234,12 @@ public class LinuxViewRenderer
|
||||
|
||||
// Subscribe to MAUI Shell navigation events to update SkiaShell
|
||||
shell.Navigated += OnShellNavigated;
|
||||
shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}");
|
||||
shell.Navigating += (s, e) => DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Navigating: {e.Target}");
|
||||
|
||||
Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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}'");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'");
|
||||
}
|
||||
|
||||
return skiaShell;
|
||||
@@ -250,33 +251,33 @@ public class LinuxViewRenderer
|
||||
private static void ApplyShellColors(SkiaShell skiaShell, Shell shell)
|
||||
{
|
||||
bool isDark = Application.Current?.UserAppTheme == AppTheme.Dark;
|
||||
Console.WriteLine($"[ApplyShellColors] Theme is: {(isDark ? "Dark" : "Light")}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
|
||||
}
|
||||
|
||||
// Flyout text color
|
||||
skiaShell.FlyoutTextColor = isDark
|
||||
? Color.FromRgb(224, 224, 224)
|
||||
: Color.FromRgb(33, 33, 33);
|
||||
Console.WriteLine($"[ApplyShellColors] FlyoutTextColor: {skiaShell.FlyoutTextColor}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
|
||||
|
||||
// NavBar background color
|
||||
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
|
||||
@@ -294,27 +295,27 @@ public class LinuxViewRenderer
|
||||
/// </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}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell or CurrentMauiShell is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current route from the Shell
|
||||
var currentState = CurrentMauiShell.CurrentState;
|
||||
var location = currentState?.Location?.OriginalString ?? "";
|
||||
Console.WriteLine($"[Navigation] Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
|
||||
|
||||
// Find the matching section in SkiaShell by route
|
||||
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
|
||||
{
|
||||
var section = CurrentSkiaShell.Sections[i];
|
||||
Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"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}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by route! Navigating to section {i}");
|
||||
if (i != CurrentSkiaShell.CurrentSectionIndex)
|
||||
{
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
@@ -323,7 +324,7 @@ public class LinuxViewRenderer
|
||||
}
|
||||
if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by title! Navigating to section {i}");
|
||||
if (i != CurrentSkiaShell.CurrentSectionIndex)
|
||||
{
|
||||
CurrentSkiaShell.NavigateToSection(i);
|
||||
@@ -331,7 +332,7 @@ public class LinuxViewRenderer
|
||||
return;
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"[Navigation] No matching section found for location: {location}");
|
||||
DiagnosticLog.Warn("LinuxViewRenderer", $"Navigation: No matching section found for location: {location}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -476,7 +477,7 @@ public class LinuxViewRenderer
|
||||
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
|
||||
{
|
||||
bgColor = cp.BackgroundColor;
|
||||
Console.WriteLine($"[CreateShellContentPage] Page BackgroundColor: {bgColor}");
|
||||
DiagnosticLog.Debug("LinuxViewRenderer", $"CreateShellContentPage: Page BackgroundColor: {bgColor}");
|
||||
}
|
||||
|
||||
if (contentView is SkiaScrollView scrollView)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
@@ -81,7 +82,7 @@ public static class MauiHandlerExtensions
|
||||
if (LinuxHandlerMap.TryGetValue(type, out Func<IElementHandler>? factory))
|
||||
{
|
||||
handler = factory();
|
||||
Console.WriteLine($"[ToHandler] Using Linux handler for {type.Name}: {handler.GetType().Name}");
|
||||
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using Linux handler for {type.Name}: {handler.GetType().Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -101,7 +102,7 @@ public static class MauiHandlerExtensions
|
||||
if (bestFactory != null)
|
||||
{
|
||||
handler = bestFactory();
|
||||
Console.WriteLine($"[ToHandler] Using Linux handler (via base {bestMatch!.Name}) for {type.Name}: {handler.GetType().Name}");
|
||||
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using Linux handler (via base {bestMatch!.Name}) for {type.Name}: {handler.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ public static class MauiHandlerExtensions
|
||||
if (handler == null)
|
||||
{
|
||||
handler = mauiContext.Handlers.GetHandler(type);
|
||||
Console.WriteLine($"[ToHandler] Using MAUI handler for {type.Name}: {handler?.GetType().Name ?? "null"}");
|
||||
DiagnosticLog.Debug("MauiHandlerExtensions", $"Using MAUI handler for {type.Name}: {handler?.GetType().Name ?? "null"}");
|
||||
}
|
||||
|
||||
if (handler != null)
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 MarketAlly LLC
|
||||
Copyright (c) 2025 MarketAlly Pte Ltd
|
||||
|
||||
Lead Architect: David H. Friedel Jr.
|
||||
|
||||
|
||||
534
LinuxApplication.Input.cs
Normal file
534
LinuxApplication.Input.cs
Normal file
@@ -0,0 +1,534 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using Microsoft.Maui.Platform;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux;
|
||||
|
||||
public partial class LinuxApplication
|
||||
{
|
||||
private void UpdateAnimations()
|
||||
{
|
||||
// Update cursor blink for text input controls
|
||||
if (_focusedView is SkiaEntry entry)
|
||||
{
|
||||
entry.UpdateCursorBlink();
|
||||
}
|
||||
else if (_focusedView is SkiaEditor editor)
|
||||
{
|
||||
editor.UpdateCursorBlink();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowResized(object? sender, (int Width, int Height) size)
|
||||
{
|
||||
if (_rootView != null)
|
||||
{
|
||||
// Re-measure with new available size, then arrange
|
||||
var availableSize = new Microsoft.Maui.Graphics.Size(size.Width, size.Height);
|
||||
_rootView.Measure(availableSize);
|
||||
_rootView.Arrange(new Microsoft.Maui.Graphics.Rect(0, 0, size.Width, size.Height));
|
||||
}
|
||||
_renderingEngine?.InvalidateAll();
|
||||
}
|
||||
|
||||
private void OnWindowExposed(object? sender, EventArgs e)
|
||||
{
|
||||
Render();
|
||||
}
|
||||
|
||||
private void OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnKeyDown(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_focusedView != null)
|
||||
{
|
||||
_focusedView.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyUp(object? sender, KeyEventArgs e)
|
||||
{
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnKeyUp(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_focusedView != null)
|
||||
{
|
||||
_focusedView.OnKeyUp(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTextInput(object? sender, TextInputEventArgs e)
|
||||
{
|
||||
if (_focusedView != null)
|
||||
{
|
||||
_focusedView.OnTextInput(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
// Route to context menu if one is active
|
||||
if (LinuxDialogService.HasContextMenu)
|
||||
{
|
||||
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnPointerMoved(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView != null)
|
||||
{
|
||||
// If a view has captured the pointer, send all events to it
|
||||
if (_capturedView != null)
|
||||
{
|
||||
_capturedView.OnPointerMoved(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for popup overlay first
|
||||
var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y);
|
||||
var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y);
|
||||
|
||||
// Track hover state changes
|
||||
if (hitView != _hoveredView)
|
||||
{
|
||||
_hoveredView?.OnPointerExited(e);
|
||||
_hoveredView = hitView;
|
||||
_hoveredView?.OnPointerEntered(e);
|
||||
|
||||
// Update cursor based on view's cursor type
|
||||
CursorType cursor = hitView?.CursorType ?? CursorType.Arrow;
|
||||
_mainWindow?.SetCursor(cursor);
|
||||
}
|
||||
|
||||
hitView?.OnPointerMoved(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerPressed(object? sender, PointerEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"OnPointerPressed at ({e.X}, {e.Y}), Button={e.Button}");
|
||||
|
||||
// Route to context menu if one is active
|
||||
if (LinuxDialogService.HasContextMenu)
|
||||
{
|
||||
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnPointerPressed(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView != null)
|
||||
{
|
||||
// Check for popup overlay first
|
||||
var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y);
|
||||
var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y);
|
||||
DiagnosticLog.Debug("LinuxApplication", $"HitView: {hitView?.GetType().Name ?? "null"}, rootView: {_rootView.GetType().Name}");
|
||||
|
||||
if (hitView != null)
|
||||
{
|
||||
// Capture pointer to this view for drag operations
|
||||
_capturedView = hitView;
|
||||
|
||||
// Update focus
|
||||
if (hitView.IsFocusable)
|
||||
{
|
||||
FocusedView = hitView;
|
||||
}
|
||||
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Calling OnPointerPressed on {hitView.GetType().Name}");
|
||||
hitView.OnPointerPressed(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Close any open popups when clicking outside
|
||||
if (SkiaView.HasActivePopup && _focusedView != null)
|
||||
{
|
||||
_focusedView.OnFocusLost();
|
||||
}
|
||||
FocusedView = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerEventArgs e)
|
||||
{
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnPointerReleased(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView != null)
|
||||
{
|
||||
// If a view has captured the pointer, send release to it
|
||||
if (_capturedView != null)
|
||||
{
|
||||
_capturedView.OnPointerReleased(e);
|
||||
_capturedView = null; // Release capture
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for popup overlay first
|
||||
var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y);
|
||||
var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y);
|
||||
hitView?.OnPointerReleased(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScroll(object? sender, ScrollEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"OnScroll - X={e.X}, Y={e.Y}, DeltaX={e.DeltaX}, DeltaY={e.DeltaY}");
|
||||
if (_rootView != null)
|
||||
{
|
||||
var hitView = _rootView.HitTest(e.X, e.Y);
|
||||
DiagnosticLog.Debug("LinuxApplication", $"HitView: {hitView?.GetType().Name ?? "null"}");
|
||||
// Bubble scroll events up to find a ScrollView
|
||||
var view = hitView;
|
||||
while (view != null)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Bubbling to: {view.GetType().Name}");
|
||||
if (view is SkiaScrollView scrollView)
|
||||
{
|
||||
scrollView.OnScroll(e);
|
||||
return;
|
||||
}
|
||||
view.OnScroll(e);
|
||||
if (e.Handled) return;
|
||||
view = view.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseRequested(object? sender, EventArgs e)
|
||||
{
|
||||
_mainWindow?.Stop();
|
||||
}
|
||||
|
||||
// GTK Event Handlers
|
||||
private void OnGtkDrawRequested(object? sender, EventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", ">>> OnGtkDrawRequested ENTER");
|
||||
LogDraw();
|
||||
var surface = _gtkWindow?.SkiaSurface;
|
||||
if (surface?.Canvas != null && _rootView != null)
|
||||
{
|
||||
var bgColor = Application.Current?.UserAppTheme == AppTheme.Dark
|
||||
? new SKColor(32, 33, 36)
|
||||
: SKColors.White;
|
||||
surface.Canvas.Clear(bgColor);
|
||||
DiagnosticLog.Debug("LinuxApplication", "Drawing rootView...");
|
||||
_rootView.Draw(surface.Canvas);
|
||||
DiagnosticLog.Debug("LinuxApplication", "Drawing dialogs...");
|
||||
var bounds = new SKRect(0, 0, surface.Width, surface.Height);
|
||||
LinuxDialogService.DrawDialogs(surface.Canvas, bounds);
|
||||
DiagnosticLog.Debug("LinuxApplication", "<<< OnGtkDrawRequested EXIT");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkResized(object? sender, (int Width, int Height) size)
|
||||
{
|
||||
PerformGtkLayout(size.Width, size.Height);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
|
||||
private void OnGtkPointerPressed(object? sender, (double X, double Y, int Button) e)
|
||||
{
|
||||
string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})";
|
||||
DiagnosticLog.Debug("LinuxApplication", $"GTK PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
LinuxDialogService.TopDialog?.OnPointerPressed(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (LinuxDialogService.HasContextMenu)
|
||||
{
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView == null)
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxApplication", "GTK _rootView is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
||||
DiagnosticLog.Debug("LinuxApplication", $"GTK HitView: {hitView?.GetType().Name ?? "null"}");
|
||||
|
||||
if (hitView != null)
|
||||
{
|
||||
if (hitView.IsFocusable && _focusedView != hitView)
|
||||
{
|
||||
_focusedView?.OnFocusLost();
|
||||
_focusedView = hitView;
|
||||
_focusedView.OnFocusGained();
|
||||
}
|
||||
_capturedView = hitView;
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
DiagnosticLog.Debug("LinuxApplication", ">>> Before OnPointerPressed");
|
||||
hitView.OnPointerPressed(args);
|
||||
DiagnosticLog.Debug("LinuxApplication", "<<< After OnPointerPressed, calling RequestRedraw");
|
||||
_gtkWindow?.RequestRedraw();
|
||||
DiagnosticLog.Debug("LinuxApplication", "<<< After RequestRedraw, returning from handler");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", ">>> OnGtkPointerReleased ENTER");
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
LinuxDialogService.TopDialog?.OnPointerReleased(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView == null) return;
|
||||
|
||||
if (_capturedView != null)
|
||||
{
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Calling OnPointerReleased on {_capturedView.GetType().Name}");
|
||||
_capturedView.OnPointerReleased(args);
|
||||
DiagnosticLog.Debug("LinuxApplication", "OnPointerReleased returned");
|
||||
_capturedView = null;
|
||||
_gtkWindow?.RequestRedraw();
|
||||
DiagnosticLog.Debug("LinuxApplication", "<<< OnGtkPointerReleased EXIT (captured path)");
|
||||
}
|
||||
else
|
||||
{
|
||||
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
||||
if (hitView != null)
|
||||
{
|
||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||
hitView.OnPointerReleased(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkPointerMoved(object? sender, (double X, double Y) e)
|
||||
{
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||
LinuxDialogService.TopDialog?.OnPointerMoved(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (LinuxDialogService.HasContextMenu)
|
||||
{
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rootView == null) return;
|
||||
|
||||
if (_capturedView != null)
|
||||
{
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||
_capturedView.OnPointerMoved(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
||||
if (hitView != _hoveredView)
|
||||
{
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||
_hoveredView?.OnPointerExited(args);
|
||||
_hoveredView = hitView;
|
||||
_hoveredView?.OnPointerEntered(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
|
||||
if (hitView != null)
|
||||
{
|
||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||
hitView.OnPointerMoved(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
||||
{
|
||||
var key = ConvertGdkKey(e.KeyVal);
|
||||
var modifiers = ConvertGdkModifiers(e.State);
|
||||
var args = new KeyEventArgs(key, modifiers);
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnKeyDown(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_focusedView != null)
|
||||
{
|
||||
_focusedView.OnKeyDown(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
||||
{
|
||||
var key = ConvertGdkKey(e.KeyVal);
|
||||
var modifiers = ConvertGdkModifiers(e.State);
|
||||
var args = new KeyEventArgs(key, modifiers);
|
||||
|
||||
// Route to dialog if one is active
|
||||
if (LinuxDialogService.HasActiveDialog)
|
||||
{
|
||||
LinuxDialogService.TopDialog?.OnKeyUp(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_focusedView != null)
|
||||
{
|
||||
_focusedView.OnKeyUp(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY, uint State) e)
|
||||
{
|
||||
if (_rootView == null) return;
|
||||
|
||||
// Convert GDK state to KeyModifiers
|
||||
var modifiers = ConvertGdkStateToModifiers(e.State);
|
||||
bool isCtrlPressed = (modifiers & KeyModifiers.Control) != 0;
|
||||
|
||||
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
||||
|
||||
// Check for pinch gesture (Ctrl+Scroll) first
|
||||
if (isCtrlPressed && hitView?.MauiView != null)
|
||||
{
|
||||
if (Handlers.GestureManager.ProcessScrollAsPinch(hitView.MauiView, e.X, e.Y, e.DeltaY, true))
|
||||
{
|
||||
_gtkWindow?.RequestRedraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (hitView != null)
|
||||
{
|
||||
if (hitView is SkiaScrollView scrollView)
|
||||
{
|
||||
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY, modifiers);
|
||||
scrollView.OnScroll(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
break;
|
||||
}
|
||||
hitView = hitView.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyModifiers ConvertGdkStateToModifiers(uint state)
|
||||
{
|
||||
var modifiers = KeyModifiers.None;
|
||||
// GDK modifier masks
|
||||
const uint GDK_SHIFT_MASK = 1 << 0;
|
||||
const uint GDK_CONTROL_MASK = 1 << 2;
|
||||
const uint GDK_MOD1_MASK = 1 << 3; // Alt
|
||||
const uint GDK_SUPER_MASK = 1 << 26;
|
||||
const uint GDK_LOCK_MASK = 1 << 1; // Caps Lock
|
||||
|
||||
if ((state & GDK_SHIFT_MASK) != 0) modifiers |= KeyModifiers.Shift;
|
||||
if ((state & GDK_CONTROL_MASK) != 0) modifiers |= KeyModifiers.Control;
|
||||
if ((state & GDK_MOD1_MASK) != 0) modifiers |= KeyModifiers.Alt;
|
||||
if ((state & GDK_SUPER_MASK) != 0) modifiers |= KeyModifiers.Super;
|
||||
if ((state & GDK_LOCK_MASK) != 0) modifiers |= KeyModifiers.CapsLock;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private void OnGtkTextInput(object? sender, string text)
|
||||
{
|
||||
if (_focusedView != null)
|
||||
{
|
||||
var args = new TextInputEventArgs(text);
|
||||
_focusedView.OnTextInput(args);
|
||||
_gtkWindow?.RequestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private static Key ConvertGdkKey(uint keyval)
|
||||
{
|
||||
return keyval switch
|
||||
{
|
||||
65288 => Key.Backspace,
|
||||
65289 => Key.Tab,
|
||||
65293 => Key.Enter,
|
||||
65307 => Key.Escape,
|
||||
65360 => Key.Home,
|
||||
65361 => Key.Left,
|
||||
65362 => Key.Up,
|
||||
65363 => Key.Right,
|
||||
65364 => Key.Down,
|
||||
65365 => Key.PageUp,
|
||||
65366 => Key.PageDown,
|
||||
65367 => Key.End,
|
||||
65535 => Key.Delete,
|
||||
>= 32 and <= 126 => (Key)keyval,
|
||||
_ => Key.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static KeyModifiers ConvertGdkModifiers(uint state)
|
||||
{
|
||||
var modifiers = KeyModifiers.None;
|
||||
if ((state & 1) != 0) modifiers |= KeyModifiers.Shift;
|
||||
if ((state & 4) != 0) modifiers |= KeyModifiers.Control;
|
||||
if ((state & 8) != 0) modifiers |= KeyModifiers.Alt;
|
||||
return modifiers;
|
||||
}
|
||||
}
|
||||
510
LinuxApplication.Lifecycle.cs
Normal file
510
LinuxApplication.Lifecycle.cs
Normal file
@@ -0,0 +1,510 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Dispatching;
|
||||
using Microsoft.Maui.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Dispatching;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using Microsoft.Maui.Platform;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux;
|
||||
|
||||
public partial class LinuxApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a MAUI application on Linux.
|
||||
/// This is the main entry point for Linux apps.
|
||||
/// </summary>
|
||||
/// <param name="app">The MauiApp to run.</param>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
public static void Run(MauiApp app, string[] args)
|
||||
{
|
||||
Run(app, args, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a MAUI application on Linux with options.
|
||||
/// </summary>
|
||||
/// <param name="app">The MauiApp to run.</param>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
/// <param name="configure">Optional configuration action.</param>
|
||||
public static void Run(MauiApp app, string[] args, Action<LinuxApplicationOptions>? configure)
|
||||
{
|
||||
// Force X11 backend for GTK/WebKitGTK - MUST be set before any GTK code runs
|
||||
Environment.SetEnvironmentVariable("GDK_BACKEND", "x11");
|
||||
|
||||
// Pre-initialize GTK for WebView compatibility (even when using X11 mode)
|
||||
int argc = 0;
|
||||
IntPtr argv = IntPtr.Zero;
|
||||
if (!GtkNative.gtk_init_check(ref argc, ref argv))
|
||||
{
|
||||
DiagnosticLog.Warn("LinuxApplication", "GTK initialization failed - WebView may not work");
|
||||
}
|
||||
else
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", "GTK pre-initialized for WebView support");
|
||||
}
|
||||
|
||||
// Set application name for desktop integration (taskbar, etc.)
|
||||
// Try to get the name from environment or use executable name
|
||||
string? appName = Environment.GetEnvironmentVariable("APPIMAGE_NAME");
|
||||
if (string.IsNullOrEmpty(appName))
|
||||
{
|
||||
appName = Path.GetFileNameWithoutExtension(Environment.ProcessPath ?? "MauiApp");
|
||||
}
|
||||
string prgName = appName.Replace(" ", "");
|
||||
GtkNative.g_set_prgname(prgName);
|
||||
GtkNative.g_set_application_name(appName);
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Set application name: {appName} (prgname: {prgName})");
|
||||
|
||||
// Initialize dispatcher
|
||||
LinuxDispatcher.Initialize();
|
||||
DispatcherProvider.SetCurrent(LinuxDispatcherProvider.Instance);
|
||||
DiagnosticLog.Debug("LinuxApplication", "Dispatcher initialized");
|
||||
|
||||
var options = app.Services.GetService<LinuxApplicationOptions>()
|
||||
?? new LinuxApplicationOptions();
|
||||
configure?.Invoke(options);
|
||||
ParseCommandLineOptions(args, options);
|
||||
|
||||
var linuxApp = new LinuxApplication();
|
||||
try
|
||||
{
|
||||
linuxApp.Initialize(options);
|
||||
|
||||
// Create MAUI context
|
||||
var mauiContext = new LinuxMauiContext(app.Services, linuxApp);
|
||||
|
||||
// Get the application and render it
|
||||
var application = app.Services.GetService<IApplication>();
|
||||
SkiaView? rootView = null;
|
||||
|
||||
if (application is Application mauiApplication)
|
||||
{
|
||||
// Force Application.Current to be this instance
|
||||
var currentProperty = typeof(Application).GetProperty("Current");
|
||||
if (currentProperty != null && currentProperty.CanWrite)
|
||||
{
|
||||
currentProperty.SetValue(null, mauiApplication);
|
||||
}
|
||||
|
||||
// Set initial theme based on system theme
|
||||
var systemTheme = SystemThemeService.Instance.CurrentTheme;
|
||||
DiagnosticLog.Debug("LinuxApplication", $"System theme detected at startup: {systemTheme}");
|
||||
if (systemTheme == SystemTheme.Dark)
|
||||
{
|
||||
mauiApplication.UserAppTheme = AppTheme.Dark;
|
||||
DiagnosticLog.Debug("LinuxApplication", "Set initial UserAppTheme to Dark based on system theme");
|
||||
}
|
||||
else
|
||||
{
|
||||
mauiApplication.UserAppTheme = AppTheme.Light;
|
||||
DiagnosticLog.Debug("LinuxApplication", "Set initial UserAppTheme to Light based on system theme");
|
||||
}
|
||||
|
||||
// Initialize GTK theme service and apply initial CSS
|
||||
GtkThemeService.ApplyTheme();
|
||||
|
||||
// Handle user-initiated theme changes
|
||||
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == "UserAppTheme")
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"User theme changed to: {mauiApplication.UserAppTheme}");
|
||||
|
||||
// Apply GTK CSS for dialogs, menus, and window decorations
|
||||
GtkThemeService.ApplyTheme();
|
||||
|
||||
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
||||
|
||||
// Force re-render the entire page to pick up theme changes
|
||||
linuxApp.RefreshPageForThemeChange();
|
||||
|
||||
// Invalidate to redraw - use correct method based on mode
|
||||
if (linuxApp._useGtk)
|
||||
{
|
||||
linuxApp._gtkWindow?.RequestRedraw();
|
||||
}
|
||||
else
|
||||
{
|
||||
linuxApp._renderingEngine?.InvalidateAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle system theme changes (e.g., GNOME/KDE dark mode toggle)
|
||||
SystemThemeService.Instance.ThemeChanged += (s, e) =>
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"System theme changed to: {e.NewTheme}");
|
||||
|
||||
// Update MAUI's UserAppTheme to match system theme
|
||||
// This will trigger the PropertyChanged handler which does the refresh
|
||||
var newAppTheme = e.NewTheme == SystemTheme.Dark ? AppTheme.Dark : AppTheme.Light;
|
||||
if (mauiApplication.UserAppTheme != newAppTheme)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Setting UserAppTheme to {newAppTheme} to match system");
|
||||
mauiApplication.UserAppTheme = newAppTheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If UserAppTheme didn't change (user manually set it), still refresh
|
||||
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
||||
linuxApp.RefreshPageForThemeChange();
|
||||
if (linuxApp._useGtk)
|
||||
{
|
||||
linuxApp._gtkWindow?.RequestRedraw();
|
||||
}
|
||||
else
|
||||
{
|
||||
linuxApp._renderingEngine?.InvalidateAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get the main page - prefer CreateWindow() over deprecated MainPage
|
||||
Page? mainPage = null;
|
||||
|
||||
// Try CreateWindow() first (the modern MAUI pattern)
|
||||
try
|
||||
{
|
||||
// CreateWindow is protected, use reflection
|
||||
var createWindowMethod = typeof(Application).GetMethod("CreateWindow",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
|
||||
null, new[] { typeof(IActivationState) }, null);
|
||||
|
||||
if (createWindowMethod != null)
|
||||
{
|
||||
var mauiWindow = createWindowMethod.Invoke(mauiApplication, new object?[] { null }) as Microsoft.Maui.Controls.Window;
|
||||
if (mauiWindow != null)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Got Window from CreateWindow: {mauiWindow.GetType().Name}");
|
||||
mainPage = mauiWindow.Page;
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Window.Page: {mainPage?.GetType().Name}");
|
||||
|
||||
// Add to windows list
|
||||
var windowsField = typeof(Application).GetField("_windows",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
|
||||
if (windowsList != null && !windowsList.Contains(mauiWindow))
|
||||
{
|
||||
windowsList.Add(mauiWindow);
|
||||
mauiWindow.Parent = mauiApplication;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("LinuxApplication", $"CreateWindow failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Fall back to deprecated MainPage if CreateWindow didn't work
|
||||
if (mainPage == null && mauiApplication.MainPage != null)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Falling back to MainPage: {mauiApplication.MainPage.GetType().Name}");
|
||||
mainPage = mauiApplication.MainPage;
|
||||
|
||||
var windowsField = typeof(Application).GetField("_windows",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
|
||||
|
||||
if (windowsList != null && windowsList.Count == 0)
|
||||
{
|
||||
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
|
||||
windowsList.Add(mauiWindow);
|
||||
mauiWindow.Parent = mauiApplication;
|
||||
}
|
||||
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
|
||||
{
|
||||
windowsList[0].Page = mainPage;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainPage != null)
|
||||
{
|
||||
var renderer = new LinuxViewRenderer(mauiContext);
|
||||
rootView = renderer.RenderPage(mainPage);
|
||||
|
||||
string windowTitle = "OpenMaui App";
|
||||
if (mainPage is NavigationPage navPage)
|
||||
{
|
||||
windowTitle = navPage.Title ?? windowTitle;
|
||||
}
|
||||
else if (mainPage is Shell shell)
|
||||
{
|
||||
windowTitle = shell.Title ?? windowTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
windowTitle = mainPage.Title ?? windowTitle;
|
||||
}
|
||||
linuxApp.SetWindowTitle(windowTitle);
|
||||
}
|
||||
}
|
||||
|
||||
if (rootView == null)
|
||||
{
|
||||
rootView = LinuxProgramHost.CreateDemoView();
|
||||
}
|
||||
|
||||
linuxApp.RootView = rootView;
|
||||
linuxApp.Run();
|
||||
}
|
||||
finally
|
||||
{
|
||||
linuxApp?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
|
||||
{
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i].ToLowerInvariant())
|
||||
{
|
||||
case "--title" when i + 1 < args.Length:
|
||||
options.Title = args[++i];
|
||||
break;
|
||||
case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w):
|
||||
options.Width = w;
|
||||
i++;
|
||||
break;
|
||||
case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h):
|
||||
options.Height = h;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the main window and runs the event loop.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
if (_useGtk)
|
||||
{
|
||||
RunGtk();
|
||||
}
|
||||
else
|
||||
{
|
||||
RunX11();
|
||||
}
|
||||
}
|
||||
|
||||
private void RunX11()
|
||||
{
|
||||
if (_mainWindow == null)
|
||||
throw new InvalidOperationException("Application not initialized");
|
||||
|
||||
_mainWindow.Show();
|
||||
Render();
|
||||
|
||||
DiagnosticLog.Debug("LinuxApplication", "Starting event loop");
|
||||
while (_mainWindow.IsRunning)
|
||||
{
|
||||
_loopCounter++;
|
||||
if (_loopCounter % 1000 == 0)
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Loop iteration {_loopCounter}");
|
||||
}
|
||||
|
||||
_mainWindow.ProcessEvents();
|
||||
SkiaWebView.ProcessGtkEvents();
|
||||
UpdateAnimations();
|
||||
Render();
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
DiagnosticLog.Debug("LinuxApplication", "Event loop ended");
|
||||
}
|
||||
|
||||
private void RunGtk()
|
||||
{
|
||||
if (_gtkWindow == null)
|
||||
throw new InvalidOperationException("Application not initialized");
|
||||
|
||||
StartHeartbeat();
|
||||
PerformGtkLayout(_gtkWindow.Width, _gtkWindow.Height);
|
||||
_gtkWindow.RequestRedraw();
|
||||
_gtkWindow.Run();
|
||||
GtkHostService.Instance.Shutdown();
|
||||
}
|
||||
|
||||
private void PerformGtkLayout(int width, int height)
|
||||
{
|
||||
if (_rootView != null)
|
||||
{
|
||||
_rootView.Measure(new Microsoft.Maui.Graphics.Size(width, height));
|
||||
_rootView.Arrange(new Microsoft.Maui.Graphics.Rect(0, 0, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces all views to refresh their theme-dependent properties.
|
||||
/// This is needed because AppThemeBinding may not automatically trigger
|
||||
/// property mappers on all platforms.
|
||||
/// </summary>
|
||||
private void RefreshPageForThemeChange()
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", "RefreshPageForThemeChange - forcing property updates");
|
||||
|
||||
// First, try to trigger MAUI's RequestedThemeChanged event using reflection
|
||||
// This ensures AppThemeBinding bindings re-evaluate
|
||||
TriggerMauiThemeChanged();
|
||||
|
||||
if (_rootView == null) return;
|
||||
|
||||
// Traverse the visual tree and force theme-dependent properties to update
|
||||
RefreshViewTheme(_rootView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after theme change to refresh views.
|
||||
/// Note: MAUI's Application.UserAppTheme setter automatically triggers RequestedThemeChanged
|
||||
/// via WeakEventManager, which AppThemeBinding subscribes to. This method handles
|
||||
/// any additional platform-specific refresh needed.
|
||||
/// </summary>
|
||||
private void TriggerMauiThemeChanged()
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Theme is now: {app.UserAppTheme}, RequestedTheme: {app.RequestedTheme}");
|
||||
}
|
||||
|
||||
private void RefreshViewTheme(SkiaView view)
|
||||
{
|
||||
// Get the associated MAUI view and handler
|
||||
var mauiView = view.MauiView;
|
||||
var handler = mauiView?.Handler;
|
||||
|
||||
if (handler != null && mauiView != null)
|
||||
{
|
||||
// Force key properties to be re-mapped
|
||||
// This ensures theme-dependent bindings are re-evaluated
|
||||
try
|
||||
{
|
||||
// Background/BackgroundColor - both need updating for AppThemeBinding
|
||||
handler.UpdateValue(nameof(IView.Background));
|
||||
handler.UpdateValue("BackgroundColor");
|
||||
|
||||
// For ImageButton, force Source to be re-mapped
|
||||
if (mauiView is Microsoft.Maui.Controls.ImageButton)
|
||||
{
|
||||
handler.UpdateValue(nameof(IImageSourcePart.Source));
|
||||
}
|
||||
|
||||
// For Image, force Source to be re-mapped
|
||||
if (mauiView is Microsoft.Maui.Controls.Image)
|
||||
{
|
||||
handler.UpdateValue(nameof(IImageSourcePart.Source));
|
||||
}
|
||||
|
||||
// For views with text colors
|
||||
if (mauiView is ITextStyle)
|
||||
{
|
||||
handler.UpdateValue(nameof(ITextStyle.TextColor));
|
||||
}
|
||||
|
||||
// For Entry/Editor placeholder colors
|
||||
if (mauiView is IPlaceholder)
|
||||
{
|
||||
handler.UpdateValue(nameof(IPlaceholder.PlaceholderColor));
|
||||
}
|
||||
|
||||
// For Border stroke
|
||||
if (mauiView is IBorderStroke)
|
||||
{
|
||||
handler.UpdateValue(nameof(IBorderStroke.Stroke));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("LinuxApplication", $"Error refreshing theme for {mauiView.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for ItemsViews (CollectionView, ListView)
|
||||
// Their item views are cached separately and need to be refreshed
|
||||
if (view is SkiaItemsView itemsView)
|
||||
{
|
||||
itemsView.RefreshTheme();
|
||||
}
|
||||
|
||||
// Special handling for NavigationPage - it stores content in _currentPage
|
||||
if (view is SkiaNavigationPage navPage && navPage.CurrentPage != null)
|
||||
{
|
||||
RefreshViewTheme(navPage.CurrentPage);
|
||||
navPage.Invalidate(); // Force redraw of navigation page
|
||||
}
|
||||
|
||||
// Special handling for SkiaPage - refresh via MauiPage handler and process Content
|
||||
if (view is SkiaPage page)
|
||||
{
|
||||
// Refresh page properties via handler if MauiPage is set
|
||||
var pageHandler = page.MauiPage?.Handler;
|
||||
if (pageHandler != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
DiagnosticLog.Debug("LinuxApplication", $"Refreshing page theme: {page.MauiPage?.GetType().Name}");
|
||||
pageHandler.UpdateValue(nameof(IView.Background));
|
||||
pageHandler.UpdateValue("BackgroundColor");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("LinuxApplication", $"Error refreshing page theme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
page.Invalidate(); // Force redraw to pick up theme-aware background
|
||||
if (page.Content != null)
|
||||
{
|
||||
RefreshViewTheme(page.Content);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process children
|
||||
// Note: SkiaLayoutView hides SkiaView.Children with 'new', so we need to cast
|
||||
IReadOnlyList<SkiaView> children = view is SkiaLayoutView layout ? layout.Children : view.Children;
|
||||
foreach (var child in children)
|
||||
{
|
||||
RefreshViewTheme(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
if (_renderingEngine != null && _rootView != null)
|
||||
{
|
||||
_renderingEngine.Render(_rootView);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_renderingEngine?.Dispose();
|
||||
_mainWindow?.Dispose();
|
||||
|
||||
if (Current == this)
|
||||
Current = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
1026
LinuxApplication.cs
1026
LinuxApplication.cs
File diff suppressed because it is too large
Load Diff
@@ -20,4 +20,15 @@ public class LinuxApplicationOptions
|
||||
public string? IconPath { get; set; }
|
||||
|
||||
public bool UseGtk { get; set; }
|
||||
|
||||
// Gesture configuration
|
||||
public double SwipeMinDistance { get; set; } = 50.0;
|
||||
public double SwipeMaxTime { get; set; } = 500.0;
|
||||
public double SwipeDirectionThreshold { get; set; } = 0.5;
|
||||
public double PanMinDistance { get; set; } = 10.0;
|
||||
public double PinchScrollScale { get; set; } = 0.1;
|
||||
|
||||
// Rendering configuration
|
||||
public int MaxDirtyRegions { get; set; } = 32;
|
||||
public float RegionMergeThreshold { get; set; } = 0.3f;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Native;
|
||||
|
||||
@@ -38,7 +39,7 @@ public static class GLibNative
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GLibNative] Error in idle callback: " + ex.Message);
|
||||
DiagnosticLog.Error("GLibNative", "Error in idle callback", ex);
|
||||
}
|
||||
if (!flag)
|
||||
{
|
||||
@@ -68,7 +69,7 @@ public static class GLibNative
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[GLibNative] Error in timeout callback: " + ex.Message);
|
||||
DiagnosticLog.Error("GLibNative", "Error in timeout callback", ex);
|
||||
}
|
||||
if (!flag)
|
||||
{
|
||||
|
||||
@@ -167,9 +167,23 @@ internal static class GtkNative
|
||||
|
||||
public static uint IdleAdd(Func<bool> callback)
|
||||
{
|
||||
GSourceFunc gSourceFunc = (IntPtr _) => callback();
|
||||
_idleCallbacks.Add(gSourceFunc);
|
||||
return IdleAdd(gSourceFunc, IntPtr.Zero);
|
||||
GSourceFunc wrapper = null!;
|
||||
wrapper = (IntPtr _) =>
|
||||
{
|
||||
bool result = callback();
|
||||
if (!result)
|
||||
{
|
||||
_idleCallbacks.Remove(wrapper);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
_idleCallbacks.Add(wrapper);
|
||||
return IdleAdd(wrapper, IntPtr.Zero);
|
||||
}
|
||||
|
||||
public static void ClearCallbacks()
|
||||
{
|
||||
_idleCallbacks.Clear();
|
||||
}
|
||||
|
||||
[DllImport("libgtk-3.so.0")]
|
||||
|
||||
196
Native/SafeHandles.cs
Normal file
196
Native/SafeHandles.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Safe handle wrapper for GTK widget pointers.
|
||||
/// Releases the widget via <c>gtk_widget_destroy</c> when disposed.
|
||||
/// </summary>
|
||||
internal class SafeGtkWidgetHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
[DllImport("libgtk-3.so.0")]
|
||||
private static extern void gtk_widget_destroy(IntPtr widget);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeGtkWidgetHandle"/> that owns the handle.
|
||||
/// </summary>
|
||||
public SafeGtkWidgetHandle() : base(ownsHandle: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeGtkWidgetHandle"/> wrapping an existing pointer.
|
||||
/// </summary>
|
||||
/// <param name="existingHandle">The existing GTK widget pointer.</param>
|
||||
/// <param name="ownsHandle">Whether this safe handle is responsible for releasing the resource.</param>
|
||||
public SafeGtkWidgetHandle(IntPtr existingHandle, bool ownsHandle = true) : base(ownsHandle)
|
||||
{
|
||||
SetHandle(existingHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
gtk_widget_destroy(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe handle wrapper for GObject pointers.
|
||||
/// Releases the object via <c>g_object_unref</c> when disposed.
|
||||
/// Suitable for any GObject-derived type including GtkCssProvider, GdkPixbuf, etc.
|
||||
/// </summary>
|
||||
internal class SafeGObjectHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
[DllImport("libgobject-2.0.so.0")]
|
||||
private static extern void g_object_unref(IntPtr obj);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeGObjectHandle"/> that owns the handle.
|
||||
/// </summary>
|
||||
public SafeGObjectHandle() : base(ownsHandle: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeGObjectHandle"/> wrapping an existing pointer.
|
||||
/// </summary>
|
||||
/// <param name="existingHandle">The existing GObject pointer.</param>
|
||||
/// <param name="ownsHandle">Whether this safe handle is responsible for releasing the resource.</param>
|
||||
public SafeGObjectHandle(IntPtr existingHandle, bool ownsHandle = true) : base(ownsHandle)
|
||||
{
|
||||
SetHandle(existingHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
g_object_unref(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe handle wrapper for X11 <c>Display*</c> pointers.
|
||||
/// Releases the display connection via <c>XCloseDisplay</c> when disposed.
|
||||
/// </summary>
|
||||
internal class SafeX11DisplayHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XCloseDisplay(IntPtr display);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeX11DisplayHandle"/> that owns the handle.
|
||||
/// </summary>
|
||||
public SafeX11DisplayHandle() : base(ownsHandle: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeX11DisplayHandle"/> wrapping an existing pointer.
|
||||
/// </summary>
|
||||
/// <param name="existingHandle">The existing X11 Display pointer.</param>
|
||||
/// <param name="ownsHandle">Whether this safe handle is responsible for releasing the resource.</param>
|
||||
public SafeX11DisplayHandle(IntPtr existingHandle, bool ownsHandle = true) : base(ownsHandle)
|
||||
{
|
||||
SetHandle(existingHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
XCloseDisplay(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe handle wrapper for X11 Cursor resources.
|
||||
/// Releases the cursor via <c>XFreeCursor</c> when disposed.
|
||||
/// Requires the associated <c>Display*</c> to be provided at construction time,
|
||||
/// as X11 cursor cleanup requires both the display and cursor handles.
|
||||
/// </summary>
|
||||
internal class SafeX11CursorHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XFreeCursor(IntPtr display, IntPtr cursor);
|
||||
|
||||
private readonly IntPtr _display;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeX11CursorHandle"/> that owns the handle.
|
||||
/// </summary>
|
||||
/// <param name="display">
|
||||
/// The X11 Display pointer required for releasing the cursor.
|
||||
/// The caller must ensure the display remains valid for the lifetime of this handle.
|
||||
/// </param>
|
||||
public SafeX11CursorHandle(IntPtr display) : base(ownsHandle: true)
|
||||
{
|
||||
_display = display;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeX11CursorHandle"/> wrapping an existing cursor.
|
||||
/// </summary>
|
||||
/// <param name="display">
|
||||
/// The X11 Display pointer required for releasing the cursor.
|
||||
/// The caller must ensure the display remains valid for the lifetime of this handle.
|
||||
/// </param>
|
||||
/// <param name="existingHandle">The existing X11 Cursor handle.</param>
|
||||
/// <param name="ownsHandle">Whether this safe handle is responsible for releasing the resource.</param>
|
||||
public SafeX11CursorHandle(IntPtr display, IntPtr existingHandle, bool ownsHandle = true) : base(ownsHandle)
|
||||
{
|
||||
_display = display;
|
||||
SetHandle(existingHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
if (_display != IntPtr.Zero)
|
||||
{
|
||||
XFreeCursor(_display, handle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe handle wrapper for <c>GtkCssProvider*</c> pointers.
|
||||
/// Since GtkCssProvider is a GObject, this releases it via <c>g_object_unref</c> when disposed.
|
||||
/// </summary>
|
||||
internal class SafeCssProviderHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
[DllImport("libgobject-2.0.so.0")]
|
||||
private static extern void g_object_unref(IntPtr obj);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeCssProviderHandle"/> that owns the handle.
|
||||
/// </summary>
|
||||
public SafeCssProviderHandle() : base(ownsHandle: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SafeCssProviderHandle"/> wrapping an existing pointer.
|
||||
/// </summary>
|
||||
/// <param name="existingHandle">The existing GtkCssProvider pointer.</param>
|
||||
/// <param name="ownsHandle">Whether this safe handle is responsible for releasing the resource.</param>
|
||||
public SafeCssProviderHandle(IntPtr existingHandle, bool ownsHandle = true) : base(ownsHandle)
|
||||
{
|
||||
SetHandle(existingHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
g_object_unref(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Native;
|
||||
|
||||
@@ -97,6 +98,8 @@ internal static class WebKitNative
|
||||
|
||||
private static readonly Dictionary<IntPtr, LoadChangedCallback> _loadChangedCallbacks = new Dictionary<IntPtr, LoadChangedCallback>();
|
||||
private static readonly Dictionary<IntPtr, ScriptDialogCallback> _scriptDialogCallbacks = new Dictionary<IntPtr, ScriptDialogCallback>();
|
||||
private static readonly Dictionary<IntPtr, ulong> _loadChangedSignalIds = new Dictionary<IntPtr, ulong>();
|
||||
private static readonly Dictionary<IntPtr, ulong> _scriptDialogSignalIds = new Dictionary<IntPtr, ulong>();
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a JavaScript dialog (alert, confirm, prompt) is requested.
|
||||
@@ -114,9 +117,15 @@ internal static class WebKitNative
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern IntPtr dlsym(IntPtr handle, string symbol);
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern int dlclose(IntPtr handle);
|
||||
|
||||
[DllImport("libdl.so.2")]
|
||||
private static extern IntPtr dlerror();
|
||||
|
||||
[DllImport("libgobject-2.0.so.0")]
|
||||
private static extern void g_signal_handler_disconnect(IntPtr instance, ulong handlerId);
|
||||
|
||||
public static bool Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
@@ -131,14 +140,14 @@ internal static class WebKitNative
|
||||
_handle = dlopen(text, 258);
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[WebKitNative] Loaded " + text);
|
||||
DiagnosticLog.Debug("WebKitNative", "Loaded " + text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[WebKitNative] Failed to load WebKitGTK library");
|
||||
DiagnosticLog.Warn("WebKitNative", "Failed to load WebKitGTK library");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -170,7 +179,7 @@ internal static class WebKitNative
|
||||
if (intPtr != IntPtr.Zero)
|
||||
{
|
||||
_gSignalConnectData = Marshal.GetDelegateForFunctionPointer<GSignalConnectDataDelegate>(intPtr);
|
||||
Console.WriteLine("[WebKitNative] Loaded g_signal_connect_data");
|
||||
DiagnosticLog.Debug("WebKitNative", "Loaded g_signal_connect_data");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,15 +306,22 @@ internal static class WebKitNative
|
||||
{
|
||||
if (_gSignalConnectData == null || webView == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[WebKitNative] Cannot connect load-changed: signal connect not available");
|
||||
DiagnosticLog.Warn("WebKitNative", "Cannot connect load-changed: signal connect not available");
|
||||
return 0uL;
|
||||
}
|
||||
_loadChangedCallbacks[webView] = callback;
|
||||
return _gSignalConnectData(webView, "load-changed", callback, IntPtr.Zero, IntPtr.Zero, 0);
|
||||
ulong signalId = _gSignalConnectData(webView, "load-changed", callback, IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_loadChangedSignalIds[webView] = signalId;
|
||||
return signalId;
|
||||
}
|
||||
|
||||
public static void DisconnectLoadChanged(IntPtr webView)
|
||||
{
|
||||
if (_loadChangedSignalIds.TryGetValue(webView, out ulong signalId) && signalId != 0)
|
||||
{
|
||||
g_signal_handler_disconnect(webView, signalId);
|
||||
_loadChangedSignalIds.Remove(webView);
|
||||
}
|
||||
_loadChangedCallbacks.Remove(webView);
|
||||
}
|
||||
|
||||
@@ -317,15 +333,22 @@ internal static class WebKitNative
|
||||
{
|
||||
if (_gSignalConnectData == null || webView == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[WebKitNative] Cannot connect script-dialog: signal connect not available");
|
||||
DiagnosticLog.Warn("WebKitNative", "Cannot connect script-dialog: signal connect not available");
|
||||
return 0uL;
|
||||
}
|
||||
_scriptDialogCallbacks[webView] = callback;
|
||||
return _gSignalConnectData(webView, "script-dialog", callback, IntPtr.Zero, IntPtr.Zero, 0);
|
||||
ulong signalId = _gSignalConnectData(webView, "script-dialog", callback, IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_scriptDialogSignalIds[webView] = signalId;
|
||||
return signalId;
|
||||
}
|
||||
|
||||
public static void DisconnectScriptDialog(IntPtr webView)
|
||||
{
|
||||
if (_scriptDialogSignalIds.TryGetValue(webView, out ulong signalId) && signalId != 0)
|
||||
{
|
||||
g_signal_handler_disconnect(webView, signalId);
|
||||
_scriptDialogSignalIds.Remove(webView);
|
||||
}
|
||||
_scriptDialogCallbacks.Remove(webView);
|
||||
}
|
||||
|
||||
@@ -376,4 +399,29 @@ internal static class WebKitNative
|
||||
{
|
||||
_webkitScriptDialogPromptSetText?.Invoke(dialog, text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up native library handles. Call on application shutdown.
|
||||
/// </summary>
|
||||
public static void Cleanup()
|
||||
{
|
||||
_loadChangedCallbacks.Clear();
|
||||
_scriptDialogCallbacks.Clear();
|
||||
_loadChangedSignalIds.Clear();
|
||||
_scriptDialogSignalIds.Clear();
|
||||
|
||||
if (_gobjectHandle != IntPtr.Zero)
|
||||
{
|
||||
dlclose(_gobjectHandle);
|
||||
_gobjectHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
dlclose(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@
|
||||
|
||||
<!-- NuGet Package Properties -->
|
||||
<PackageId>OpenMaui.Controls.Linux</PackageId>
|
||||
<Version>1.0.0-rc.1</Version>
|
||||
<Authors>MarketAlly LLC, David H. Friedel Jr.</Authors>
|
||||
<Company>MarketAlly LLC</Company>
|
||||
<Version>9.0.40</Version>
|
||||
<Authors>MarketAlly Pte Ltd, David H. Friedel Jr.</Authors>
|
||||
<Company>MarketAlly Pte Ltd</Company>
|
||||
<Product>OpenMaui Linux Controls</Product>
|
||||
<Description>Linux desktop support for .NET MAUI applications using SkiaSharp rendering. Supports X11 and Wayland display servers with 35+ controls, platform services, and accessibility support.</Description>
|
||||
<Copyright>Copyright 2025 MarketAlly LLC</Copyright>
|
||||
<Copyright>Copyright 2025-2026 MarketAlly Pte Ltd</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://git.marketally.com/open-maui/maui-linux</PackageProjectUrl>
|
||||
<RepositoryUrl>https://git.marketally.com/open-maui/maui-linux.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://git.marketally.ai/open-maui/maui-linux</PackageProjectUrl>
|
||||
<RepositoryUrl>https://git.marketally.ai/open-maui/maui-linux.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>maui;linux;desktop;skia;gui;cross-platform;dotnet;x11;wayland;openmaui</PackageTags>
|
||||
<PackageReleaseNotes>RC1: 100% .NET MAUI API compliance - all public APIs use MAUI types (Color, Rect, Size, Thickness, double). Full XAML support with BindableProperty for all controls, Visual State Manager integration, data binding, and XAML styles. 217 passing tests.</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>9.0.40: Version aligned with MAUI 9.0.40. 100% .NET MAUI API compliance - all public APIs use MAUI types. Full XAML support, Visual State Manager, data binding, XAML styles. 541 passing tests. Previously released as 1.0.0.</PackageReleaseNotes>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>OpenMaui.Controls.Linux</id>
|
||||
<version>1.0.0-preview.1</version>
|
||||
<version>9.0.40</version>
|
||||
<title>OpenMaui Linux Controls</title>
|
||||
<authors>MarketAlly LLC, David H. Friedel Jr.</authors>
|
||||
<owners>MarketAlly LLC</owners>
|
||||
<authors>MarketAlly Pte Ltd, David H. Friedel Jr.</authors>
|
||||
<owners>MarketAlly Pte Ltd</owners>
|
||||
<license type="expression">MIT</license>
|
||||
<projectUrl>https://github.com/open-maui/maui-linux</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/dotnet/maui/main/assets/icon.png</iconUrl>
|
||||
<projectUrl>https://git.marketally.ai/open-maui/maui-linux</projectUrl>
|
||||
<iconUrl>https://git.marketally.ai/open-maui/maui-linux/raw/branch/main/assets/icon.png</iconUrl>
|
||||
<description>
|
||||
Linux desktop support for .NET MAUI applications. This package enables running MAUI applications on Linux desktop environments using SkiaSharp for rendering.
|
||||
|
||||
@@ -21,25 +21,19 @@ Features:
|
||||
- Accessibility support (AT-SPI2, High Contrast)
|
||||
- HiDPI and Input Method support
|
||||
|
||||
Developed by MarketAlly LLC. Lead Architect: David H. Friedel Jr.
|
||||
Developed by MarketAlly Pte Ltd. Lead Architect: David H. Friedel Jr.
|
||||
</description>
|
||||
<releaseNotes>
|
||||
Initial release:
|
||||
- Core SkiaSharp-based rendering engine
|
||||
- X11 window management with full input handling
|
||||
- 35+ control implementations
|
||||
- 18 platform services
|
||||
- Accessibility support
|
||||
- 216 unit tests
|
||||
9.0.40: Version aligned with MAUI 9.0.40. 100% .NET MAUI API compliance - all public APIs use MAUI types. Full XAML support, SkiaSharp rendering, X11 window management, 35+ controls, 18 platform services, accessibility support. 541 passing tests. Previously released as 1.0.0.
|
||||
</releaseNotes>
|
||||
<copyright>Copyright 2025 MarketAlly LLC</copyright>
|
||||
<copyright>Copyright 2025-2026 MarketAlly Pte Ltd</copyright>
|
||||
<tags>maui linux desktop skia gui cross-platform dotnet openmaui</tags>
|
||||
<repository type="git" url="https://github.com/open-maui/maui-linux.git" />
|
||||
<repository type="git" url="https://git.marketally.ai/open-maui/maui-linux.git" />
|
||||
<dependencies>
|
||||
<group targetFramework="net9.0">
|
||||
<dependency id="Microsoft.Maui.Controls" version="9.0.0" />
|
||||
<dependency id="SkiaSharp" version="2.88.8" />
|
||||
<dependency id="SkiaSharp.NativeAssets.Linux" version="2.88.8" />
|
||||
<dependency id="Microsoft.Maui.Controls" version="9.0.40" />
|
||||
<dependency id="SkiaSharp" version="2.88.9" />
|
||||
<dependency id="SkiaSharp.NativeAssets.Linux" version="2.88.9" />
|
||||
</group>
|
||||
</dependencies>
|
||||
<frameworkAssemblies>
|
||||
|
||||
16
README.md
16
README.md
@@ -5,7 +5,7 @@ A comprehensive Linux platform implementation for .NET MAUI using SkiaSharp rend
|
||||
[](https://www.nuget.org/packages/OpenMaui.Controls.Linux)
|
||||
[](LICENSE)
|
||||
|
||||
**Developed by [MarketAlly LLC](https://marketally.com)**
|
||||
**Developed by [MarketAlly Pte Ltd](https://marketally.sg)**
|
||||
**Lead Architect: David H. Friedel Jr.**
|
||||
|
||||
## Overview
|
||||
@@ -40,7 +40,7 @@ dotnet run
|
||||
### Manual Installation
|
||||
|
||||
```bash
|
||||
dotnet add package OpenMaui.Controls.Linux --prerelease
|
||||
dotnet add package OpenMaui.Controls.Linux
|
||||
```
|
||||
|
||||
## XAML Support
|
||||
@@ -135,12 +135,12 @@ sudo dnf install libX11-devel libXrandr-devel libXcursor-devel libXi-devel mesa-
|
||||
|
||||
## Sample Applications
|
||||
|
||||
Full sample applications are available in the [maui-linux-samples](https://git.marketally.com/open-maui/maui-linux-samples) repository:
|
||||
Full sample applications are available in the [maui-linux-samples](https://git.marketally.ai/open-maui/maui-linux-samples) repository:
|
||||
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| **[TodoApp](https://git.marketally.com/open-maui/maui-linux-samples/src/branch/main/TodoApp)** | Task manager with NavigationPage, XAML data binding, CollectionView |
|
||||
| **[ShellDemo](https://git.marketally.com/open-maui/maui-linux-samples/src/branch/main/ShellDemo)** | Control showcase with Shell navigation and flyout menu |
|
||||
| **[TodoApp](https://git.marketally.ai/open-maui/maui-linux-samples/src/branch/main/TodoApp)** | Task manager with NavigationPage, XAML data binding, CollectionView |
|
||||
| **[ShellDemo](https://git.marketally.ai/open-maui/maui-linux-samples/src/branch/main/ShellDemo)** | Control showcase with Shell navigation and flyout menu |
|
||||
|
||||
## Quick Example
|
||||
|
||||
@@ -179,7 +179,7 @@ app.Run();
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/open-maui/maui-linux.git
|
||||
git clone https://git.marketally.ai/open-maui/maui-linux.git
|
||||
cd maui-linux
|
||||
dotnet build
|
||||
dotnet test
|
||||
@@ -275,11 +275,11 @@ All interactive controls support VSM states: Normal, PointerOver, Pressed, Focus
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2025 MarketAlly LLC. Licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
Copyright (c) 2025-2026 MarketAlly Pte Ltd. Licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [MarketAlly LLC](https://marketally.com) - Project development and maintenance
|
||||
- [MarketAlly Pte Ltd](https://marketally.sg) - Project development and maintenance
|
||||
- [SkiaSharp](https://github.com/mono/SkiaSharp) - 2D graphics library
|
||||
- [.NET MAUI](https://github.com/dotnet/maui) - Cross-platform UI framework
|
||||
- The .NET community
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -58,7 +59,7 @@ public class GpuRenderingEngine : IDisposable
|
||||
|
||||
if (!_gpuAvailable)
|
||||
{
|
||||
Console.WriteLine("[GpuRenderingEngine] GPU not available, using software rendering");
|
||||
DiagnosticLog.Debug("GpuRenderingEngine", "GPU not available, using software rendering");
|
||||
InitializeSoftwareRendering();
|
||||
}
|
||||
|
||||
@@ -74,25 +75,25 @@ public class GpuRenderingEngine : IDisposable
|
||||
var glInterface = GRGlInterface.Create();
|
||||
if (glInterface == null)
|
||||
{
|
||||
Console.WriteLine("[GpuRenderingEngine] Failed to create GL interface");
|
||||
DiagnosticLog.Warn("GpuRenderingEngine", "Failed to create GL interface");
|
||||
return false;
|
||||
}
|
||||
|
||||
_grContext = GRContext.CreateGl(glInterface);
|
||||
if (_grContext == null)
|
||||
{
|
||||
Console.WriteLine("[GpuRenderingEngine] Failed to create GR context");
|
||||
DiagnosticLog.Warn("GpuRenderingEngine", "Failed to create GR context");
|
||||
glInterface.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
CreateGpuSurface();
|
||||
Console.WriteLine("[GpuRenderingEngine] GPU acceleration enabled");
|
||||
DiagnosticLog.Debug("GpuRenderingEngine", "GPU acceleration enabled");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GpuRenderingEngine] GPU initialization failed: {ex.Message}");
|
||||
DiagnosticLog.Error("GpuRenderingEngine", "GPU initialization failed", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +125,7 @@ public class GpuRenderingEngine : IDisposable
|
||||
|
||||
if (_surface == null)
|
||||
{
|
||||
Console.WriteLine("[GpuRenderingEngine] Failed to create GPU surface, falling back to software");
|
||||
DiagnosticLog.Warn("GpuRenderingEngine", "Failed to create GPU surface, falling back to software");
|
||||
_gpuAvailable = false;
|
||||
InitializeSoftwareRendering();
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Rendering;
|
||||
@@ -91,6 +92,12 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
||||
private readonly ConfigureCallback _configureCallback;
|
||||
private ulong _drawSignalId;
|
||||
private ulong _configureSignalId;
|
||||
private ulong _buttonPressSignalId;
|
||||
private ulong _buttonReleaseSignalId;
|
||||
private ulong _motionSignalId;
|
||||
private ulong _keyPressSignalId;
|
||||
private ulong _keyReleaseSignalId;
|
||||
private ulong _scrollSignalId;
|
||||
private bool _isTransparent;
|
||||
private readonly ButtonEventCallback _buttonPressCallback;
|
||||
private readonly ButtonEventCallback _buttonReleaseCallback;
|
||||
@@ -143,14 +150,14 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
||||
// Connect signals
|
||||
_drawSignalId = GtkNative.g_signal_connect_data(_widget, "draw", Marshal.GetFunctionPointerForDelegate(_drawCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_configureSignalId = GtkNative.g_signal_connect_data(_widget, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "key-press-event", Marshal.GetFunctionPointerForDelegate(_keyPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "key-release-event", Marshal.GetFunctionPointerForDelegate(_keyReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
GtkNative.g_signal_connect_data(_widget, "scroll-event", Marshal.GetFunctionPointerForDelegate(_scrollCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_buttonPressSignalId = GtkNative.g_signal_connect_data(_widget, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_buttonReleaseSignalId = GtkNative.g_signal_connect_data(_widget, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_motionSignalId = GtkNative.g_signal_connect_data(_widget, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_keyPressSignalId = GtkNative.g_signal_connect_data(_widget, "key-press-event", Marshal.GetFunctionPointerForDelegate(_keyPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_keyReleaseSignalId = GtkNative.g_signal_connect_data(_widget, "key-release-event", Marshal.GetFunctionPointerForDelegate(_keyReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
_scrollSignalId = GtkNative.g_signal_connect_data(_widget, "scroll-event", Marshal.GetFunctionPointerForDelegate(_scrollCallback), IntPtr.Zero, IntPtr.Zero, 0);
|
||||
|
||||
Console.WriteLine($"[GtkSkiaSurfaceWidget] Created with size {width}x{height}");
|
||||
DiagnosticLog.Debug("GtkSkiaSurfaceWidget", $"Created with size {width}x{height}");
|
||||
}
|
||||
|
||||
private void CreateBuffer(int width, int height)
|
||||
@@ -179,7 +186,7 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
||||
_imageInfo.Height,
|
||||
_imageInfo.RowBytes);
|
||||
|
||||
Console.WriteLine($"[GtkSkiaSurfaceWidget] Created buffer {width}x{height}, stride={_imageInfo.RowBytes}");
|
||||
DiagnosticLog.Debug("GtkSkiaSurfaceWidget", $"Created buffer {width}x{height}, stride={_imageInfo.RowBytes}");
|
||||
}
|
||||
|
||||
public void Resize(int width, int height)
|
||||
@@ -303,7 +310,7 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
||||
if (!char.IsControl(c) || c == '\r' || c == '\n' || c == '\t')
|
||||
{
|
||||
string text = c.ToString();
|
||||
Console.WriteLine($"[GtkSkiaSurfaceWidget] TextInput: '{text}' (keyval={keyval}, unicode={unicode})");
|
||||
DiagnosticLog.Debug("GtkSkiaSurfaceWidget", $"TextInput: '{text}' (keyval={keyval}, unicode={unicode})");
|
||||
TextInput?.Invoke(this, text);
|
||||
}
|
||||
}
|
||||
@@ -381,6 +388,19 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Disconnect all signal handlers before disposing
|
||||
if (_widget != IntPtr.Zero)
|
||||
{
|
||||
if (_drawSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _drawSignalId);
|
||||
if (_configureSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _configureSignalId);
|
||||
if (_buttonPressSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _buttonPressSignalId);
|
||||
if (_buttonReleaseSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _buttonReleaseSignalId);
|
||||
if (_motionSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _motionSignalId);
|
||||
if (_keyPressSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _keyPressSignalId);
|
||||
if (_keyReleaseSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _keyReleaseSignalId);
|
||||
if (_scrollSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _scrollSignalId);
|
||||
}
|
||||
|
||||
_canvas?.Dispose();
|
||||
_canvas = null;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using SkiaSharp;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Rendering;
|
||||
@@ -25,8 +26,15 @@ public class SkiaRenderingEngine : IDisposable
|
||||
// Dirty region tracking for optimized rendering
|
||||
private readonly List<SKRect> _dirtyRegions = new();
|
||||
private readonly object _dirtyLock = new();
|
||||
private const int MaxDirtyRegions = 32;
|
||||
private const float RegionMergeThreshold = 0.3f; // Merge if overlap > 30%
|
||||
/// <summary>
|
||||
/// Maximum number of dirty regions to track before falling back to a full redraw.
|
||||
/// </summary>
|
||||
public static int MaxDirtyRegions { get; set; } = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Overlap ratio threshold (0.0-1.0) at which adjacent dirty regions are merged.
|
||||
/// </summary>
|
||||
public static float RegionMergeThreshold { get; set; } = 0.3f;
|
||||
|
||||
public static SkiaRenderingEngine? Current { get; private set; }
|
||||
public ResourceCache ResourceCache { get; }
|
||||
@@ -169,8 +177,16 @@ public class SkiaRenderingEngine : IDisposable
|
||||
|
||||
// Measure and arrange
|
||||
var availableSize = new Size(Width, Height);
|
||||
rootView.Measure(availableSize);
|
||||
rootView.Arrange(new Rect(0, 0, Width, Height));
|
||||
try
|
||||
{
|
||||
rootView.Measure(availableSize);
|
||||
rootView.Arrange(new Rect(0, 0, Width, Height));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaRenderingEngine", "Exception during Measure/Arrange", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine what to redraw
|
||||
List<SKRect> regionsToRedraw;
|
||||
@@ -199,16 +215,37 @@ public class SkiaRenderingEngine : IDisposable
|
||||
// Render dirty regions
|
||||
foreach (var region in regionsToRedraw)
|
||||
{
|
||||
RenderRegion(rootView, region, isFullRedraw);
|
||||
try
|
||||
{
|
||||
RenderRegion(rootView, region, isFullRedraw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaRenderingEngine", $"Exception rendering region {region}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw popup overlays (always on top, full redraw)
|
||||
SkiaView.DrawPopupOverlays(_canvas);
|
||||
try
|
||||
{
|
||||
SkiaView.DrawPopupOverlays(_canvas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaRenderingEngine", "Exception drawing popup overlays", ex);
|
||||
}
|
||||
|
||||
// Draw modal dialogs and context menus on top of everything
|
||||
if (LinuxDialogService.HasActiveDialog || LinuxDialogService.HasContextMenu)
|
||||
try
|
||||
{
|
||||
LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height));
|
||||
if (LinuxDialogService.HasActiveDialog || LinuxDialogService.HasContextMenu)
|
||||
{
|
||||
LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaRenderingEngine", "Exception drawing dialogs", ex);
|
||||
}
|
||||
|
||||
_canvas.Flush();
|
||||
@@ -234,7 +271,14 @@ public class SkiaRenderingEngine : IDisposable
|
||||
_canvas.DrawRect(region, clearPaint);
|
||||
|
||||
// Draw the view tree (views will naturally clip to their bounds)
|
||||
rootView.Draw(_canvas);
|
||||
try
|
||||
{
|
||||
rootView.Draw(_canvas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaRenderingEngine", "Exception during view Draw", ex);
|
||||
}
|
||||
|
||||
_canvas.Restore();
|
||||
}
|
||||
|
||||
112
Rendering/TextRenderingHelper.cs
Normal file
112
Rendering/TextRenderingHelper.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Shared text rendering utilities extracted from SkiaEntry, SkiaEditor, and SkiaLabel
|
||||
/// to eliminate code duplication for common text rendering operations.
|
||||
/// </summary>
|
||||
public static class TextRenderingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws text with font fallback for emoji, CJK, and other scripts.
|
||||
/// Uses FontFallbackManager to shape text across multiple typefaces when needed.
|
||||
/// </summary>
|
||||
public static void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface, float fontSize)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use FontFallbackManager for mixed-script text
|
||||
var runs = FontFallbackManager.Instance.ShapeTextWithFallback(text, preferredTypeface);
|
||||
|
||||
if (runs.Count <= 1)
|
||||
{
|
||||
// Single run or no fallback needed - draw directly
|
||||
canvas.DrawText(text, x, y, paint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple runs with different fonts
|
||||
float currentX = x;
|
||||
foreach (var run in runs)
|
||||
{
|
||||
using var runFont = new SKFont(run.Typeface, fontSize);
|
||||
using var runPaint = new SKPaint(runFont)
|
||||
{
|
||||
Color = paint.Color,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
canvas.DrawText(run.Text, currentX, y, runPaint);
|
||||
currentX += runPaint.MeasureText(run.Text);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws underline for IME pre-edit (composition) text.
|
||||
/// Renders a dashed underline beneath the pre-edit text region.
|
||||
/// </summary>
|
||||
public static void DrawPreEditUnderline(SKCanvas canvas, SKPaint paint, string displayText, int cursorPosition, string preEditText, float x, float y)
|
||||
{
|
||||
// Calculate pre-edit text position
|
||||
var textToCursor = displayText.Substring(0, Math.Min(cursorPosition, displayText.Length));
|
||||
var preEditStartX = x + paint.MeasureText(textToCursor);
|
||||
var preEditEndX = preEditStartX + paint.MeasureText(preEditText);
|
||||
|
||||
// Draw dotted underline to indicate composition
|
||||
using var underlinePaint = new SKPaint
|
||||
{
|
||||
Color = paint.Color,
|
||||
StrokeWidth = 1,
|
||||
IsAntialias = true,
|
||||
PathEffect = SKPathEffect.CreateDash(new float[] { 3, 2 }, 0)
|
||||
};
|
||||
|
||||
var underlineY = y + 2;
|
||||
canvas.DrawLine(preEditStartX, underlineY, preEditEndX, underlineY, underlinePaint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a MAUI Color to SkiaSharp SKColor for rendering.
|
||||
/// Returns the specified default color when the input color is null.
|
||||
/// </summary>
|
||||
public static SKColor ToSKColor(Color? color, SKColor defaultColor = default)
|
||||
{
|
||||
if (color == null) return defaultColor;
|
||||
return color.ToSKColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts FontAttributes to the corresponding SKFontStyle.
|
||||
/// </summary>
|
||||
public static SKFontStyle GetFontStyle(FontAttributes attributes)
|
||||
{
|
||||
bool isBold = attributes.HasFlag(FontAttributes.Bold);
|
||||
bool isItalic = attributes.HasFlag(FontAttributes.Italic);
|
||||
|
||||
if (isBold && isItalic)
|
||||
return SKFontStyle.BoldItalic;
|
||||
if (isBold)
|
||||
return SKFontStyle.Bold;
|
||||
if (isItalic)
|
||||
return SKFontStyle.Italic;
|
||||
return SKFontStyle.Normal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective font family, returning "Sans" as the platform default when empty.
|
||||
/// </summary>
|
||||
public static string GetEffectiveFontFamily(string? fontFamily)
|
||||
{
|
||||
return string.IsNullOrEmpty(fontFamily) ? "Sans" : fontFamily;
|
||||
}
|
||||
}
|
||||
@@ -99,8 +99,9 @@ public class AppInfoService : IAppInfo
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Debug("AppInfoService", "Settings launch fallback failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
|
||||
int result = atspi_init();
|
||||
if (result != 0)
|
||||
{
|
||||
Console.WriteLine("AtSpi2AccessibilityService: Failed to initialize AT-SPI2");
|
||||
DiagnosticLog.Error("AtSpi2AccessibilityService", "Failed to initialize AT-SPI2");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,16 +51,16 @@ public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
|
||||
// Register our application
|
||||
RegisterApplication();
|
||||
|
||||
Console.WriteLine("AtSpi2AccessibilityService: Initialized successfully");
|
||||
DiagnosticLog.Debug("AtSpi2AccessibilityService", "Initialized successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("AtSpi2AccessibilityService: Accessibility is not enabled");
|
||||
DiagnosticLog.Warn("AtSpi2AccessibilityService", "Accessibility is not enabled");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AtSpi2AccessibilityService: Initialization failed - {ex.Message}");
|
||||
DiagnosticLog.Error("AtSpi2AccessibilityService", $"Initialization failed - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +168,11 @@ public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
|
||||
// or by emitting "object:announcement" events
|
||||
|
||||
// For now, use a simpler approach with the event system
|
||||
Console.WriteLine($"[Accessibility Announcement ({priority})]: {text}");
|
||||
DiagnosticLog.Debug("AtSpi2AccessibilityService", $"Announcement ({priority}): {text}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AtSpi2AccessibilityService: Announcement failed - {ex.Message}");
|
||||
DiagnosticLog.Error("AtSpi2AccessibilityService", $"Announcement failed - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
|
||||
// using the org.a11y.atspi.Event interface
|
||||
|
||||
// For now, log the event for debugging
|
||||
Console.WriteLine($"[AT-SPI2 Event] {eventName}: {accessible.AccessibleName} ({accessible.Role})");
|
||||
DiagnosticLog.Debug("AtSpi2AccessibilityService", $"Event {eventName}: {accessible.AccessibleName} ({accessible.Role})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -129,8 +129,9 @@ public class ConnectivityService : IConnectivity, IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Debug("ConnectivityService", "Gateway check failed", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -137,8 +137,9 @@ public class DeviceDisplayService : IDeviceDisplay
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Debug("DeviceDisplayService", "Display info refresh failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ public class DeviceInfoService : IDeviceInfo
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Debug("DeviceInfoService", "OS version parsing failed", ex);
|
||||
}
|
||||
return new Version(1, 0);
|
||||
}
|
||||
|
||||
91
Services/DiagnosticLog.cs
Normal file
91
Services/DiagnosticLog.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized diagnostic logging for the Linux MAUI platform.
|
||||
/// Logging is enabled only in DEBUG builds by default, or when
|
||||
/// explicitly enabled via <see cref="IsEnabled"/>.
|
||||
/// </summary>
|
||||
public static class DiagnosticLog
|
||||
{
|
||||
private static bool? _isEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether diagnostic logging is enabled.
|
||||
/// Defaults to true in DEBUG builds, false in RELEASE builds.
|
||||
/// </summary>
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isEnabled.HasValue)
|
||||
return _isEnabled.Value;
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
set => _isEnabled = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an informational diagnostic message.
|
||||
/// </summary>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Debug(string tag, string message)
|
||||
{
|
||||
if (IsEnabled)
|
||||
System.Console.WriteLine($"[{tag}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug diagnostic message with exception details.
|
||||
/// Only compiled in DEBUG builds.
|
||||
/// </summary>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Debug(string tag, string message, Exception ex)
|
||||
{
|
||||
if (IsEnabled)
|
||||
System.Console.WriteLine($"[{tag}] {message}: {ex.Message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an informational diagnostic message (always writes when enabled, not conditional on DEBUG).
|
||||
/// Use for important operational messages that should appear in release builds when logging is enabled.
|
||||
/// </summary>
|
||||
public static void Info(string tag, string message)
|
||||
{
|
||||
if (IsEnabled)
|
||||
System.Console.WriteLine($"[{tag}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message. Always writes when logging is enabled.
|
||||
/// </summary>
|
||||
public static void Warn(string tag, string message)
|
||||
{
|
||||
if (IsEnabled)
|
||||
System.Console.Error.WriteLine($"[{tag}] WARNING: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message. Always writes regardless of IsEnabled.
|
||||
/// </summary>
|
||||
public static void Error(string tag, string message)
|
||||
{
|
||||
System.Console.Error.WriteLine($"[{tag}] ERROR: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message with exception details. Always writes regardless of IsEnabled.
|
||||
/// </summary>
|
||||
public static void Error(string tag, string message, Exception ex)
|
||||
{
|
||||
System.Console.Error.WriteLine($"[{tag}] ERROR: {message}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,12 @@ public static class DisplayServerFactory
|
||||
|
||||
if (!string.IsNullOrEmpty(xDisplay) && !string.IsNullOrEmpty(preferX11))
|
||||
{
|
||||
Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)");
|
||||
DiagnosticLog.Debug("DisplayServerFactory", "XWayland detected, using X11 backend (MAUI_PREFER_X11 set)");
|
||||
_cachedServerType = DisplayServerType.X11;
|
||||
return DisplayServerType.X11;
|
||||
}
|
||||
|
||||
Console.WriteLine("[DisplayServer] Wayland display detected");
|
||||
DiagnosticLog.Debug("DisplayServerFactory", "Wayland display detected");
|
||||
_cachedServerType = DisplayServerType.Wayland;
|
||||
return DisplayServerType.Wayland;
|
||||
}
|
||||
@@ -43,13 +43,13 @@ public static class DisplayServerFactory
|
||||
var x11Display = Environment.GetEnvironmentVariable("DISPLAY");
|
||||
if (!string.IsNullOrEmpty(x11Display))
|
||||
{
|
||||
Console.WriteLine("[DisplayServer] X11 display detected");
|
||||
DiagnosticLog.Debug("DisplayServerFactory", "X11 display detected");
|
||||
_cachedServerType = DisplayServerType.X11;
|
||||
return DisplayServerType.X11;
|
||||
}
|
||||
|
||||
// Default to X11 and let it fail if not available
|
||||
Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11");
|
||||
DiagnosticLog.Warn("DisplayServerFactory", "No display server detected, defaulting to X11");
|
||||
_cachedServerType = DisplayServerType.X11;
|
||||
return DisplayServerType.X11;
|
||||
}
|
||||
@@ -76,12 +76,12 @@ public static class DisplayServerFactory
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})");
|
||||
DiagnosticLog.Debug("DisplayServerFactory", $"Creating X11 window: {title} ({width}x{height})");
|
||||
return new X11DisplayWindow(title, width, height);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DisplayServer] Failed to create X11 window: {ex.Message}");
|
||||
DiagnosticLog.Error("DisplayServerFactory", $"Failed to create X11 window: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -90,18 +90,18 @@ public static class DisplayServerFactory
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})");
|
||||
DiagnosticLog.Debug("DisplayServerFactory", $"Creating Wayland window: {title} ({width}x{height})");
|
||||
return new WaylandDisplayWindow(title, width, height);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DisplayServer] Failed to create Wayland window: {ex.Message}");
|
||||
DiagnosticLog.Error("DisplayServerFactory", $"Failed to create Wayland window: {ex.Message}");
|
||||
|
||||
// Try to fall back to X11 via XWayland
|
||||
var xDisplay = Environment.GetEnvironmentVariable("DISPLAY");
|
||||
if (!string.IsNullOrEmpty(xDisplay))
|
||||
{
|
||||
Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)");
|
||||
DiagnosticLog.Warn("DisplayServerFactory", "Falling back to X11 (XWayland)");
|
||||
return CreateX11Window(title, width, height);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,18 +48,18 @@ public class Fcitx5InputMethodService : IInputMethodService, IDisposable
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
_inputContextPath = output.Substring(start + 1, end - start - 1);
|
||||
Console.WriteLine($"Fcitx5InputMethodService: Created context at {_inputContextPath}");
|
||||
DiagnosticLog.Debug("Fcitx5InputMethodService", $"Created context at {_inputContextPath}");
|
||||
StartMonitoring();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Fcitx5InputMethodService: Failed to create input context");
|
||||
DiagnosticLog.Error("Fcitx5InputMethodService", "Failed to create input context");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fcitx5InputMethodService: Initialization failed - {ex.Message}");
|
||||
DiagnosticLog.Error("Fcitx5InputMethodService", $"Initialization failed - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class Fcitx5InputMethodService : IInputMethodService, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fcitx5InputMethodService: Monitor error - {ex.Message}");
|
||||
DiagnosticLog.Error("Fcitx5InputMethodService", $"Monitor error - {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public class Fcitx5InputMethodService : IInputMethodService, IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("Fcitx5InputMethodService", "Commit signal processing failed", ex); }
|
||||
}
|
||||
|
||||
private async Task ProcessPreeditSignal(StreamReader reader)
|
||||
@@ -160,7 +160,7 @@ public class Fcitx5InputMethodService : IInputMethodService, IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("Fcitx5InputMethodService", "Preedit signal processing failed", ex); }
|
||||
}
|
||||
|
||||
public void SetFocus(IInputContext? context)
|
||||
@@ -284,7 +284,7 @@ public class Fcitx5InputMethodService : IInputMethodService, IDisposable
|
||||
_dBusMonitor?.Kill();
|
||||
_dBusMonitor?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("Fcitx5InputMethodService", "D-Bus monitor cleanup failed", ex); }
|
||||
|
||||
if (!string.IsNullOrEmpty(_inputContextPath))
|
||||
{
|
||||
|
||||
@@ -91,7 +91,7 @@ public class FilePickerService : IFilePicker
|
||||
if (tool == DialogTool.None)
|
||||
{
|
||||
// Fall back to console path input
|
||||
Console.WriteLine("No file dialog available. Please enter file path:");
|
||||
DiagnosticLog.Warn("FilePickerService", "No file dialog available. Please enter file path:");
|
||||
var path = Console.ReadLine();
|
||||
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
@@ -81,7 +82,7 @@ public class FontFallbackManager
|
||||
|
||||
// Cache for typeface lookups
|
||||
private readonly Dictionary<string, SKTypeface?> _typefaceCache = new();
|
||||
private readonly Dictionary<(int codepoint, string preferredFont), SKTypeface?> _glyphCache = new();
|
||||
private readonly ConcurrentDictionary<(int codepoint, string preferredFont), SKTypeface?> _glyphCache = new();
|
||||
|
||||
private FontFallbackManager()
|
||||
{
|
||||
|
||||
@@ -76,7 +76,7 @@ public class GlobalHotkeyService : IDisposable
|
||||
int result = XGrabKey(_display, keyCode, mask, _rootWindow, true, GrabModeAsync, GrabModeAsync);
|
||||
if (result == 0)
|
||||
{
|
||||
Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}");
|
||||
DiagnosticLog.Warn("GlobalHotkeyService", $"Failed to grab key {key} with modifiers {modifiers}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ public class GlobalHotkeyService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"GlobalHotkeyService error: {ex.Message}");
|
||||
DiagnosticLog.Error("GlobalHotkeyService", $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,17 +295,17 @@ public class Gtk4InteropService : IDisposable
|
||||
{
|
||||
_useGtk4 = true;
|
||||
_initialized = true;
|
||||
Console.WriteLine("[GTK4] Initialized GTK4");
|
||||
DiagnosticLog.Debug("Gtk4InteropService", "Initialized GTK4");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Console.WriteLine("[GTK4] GTK4 not found, trying GTK3");
|
||||
DiagnosticLog.Warn("Gtk4InteropService", "GTK4 not found, trying GTK3");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GTK4] GTK4 init failed: {ex.Message}");
|
||||
DiagnosticLog.Error("Gtk4InteropService", $"GTK4 init failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Fall back to GTK3
|
||||
@@ -317,17 +317,17 @@ public class Gtk4InteropService : IDisposable
|
||||
{
|
||||
_useGtk4 = false;
|
||||
_initialized = true;
|
||||
Console.WriteLine("[GTK4] Initialized GTK3 (fallback)");
|
||||
DiagnosticLog.Debug("Gtk4InteropService", "Initialized GTK3 (fallback)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Console.WriteLine("[GTK4] GTK3 not found");
|
||||
DiagnosticLog.Warn("Gtk4InteropService", "GTK3 not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GTK4] GTK3 init failed: {ex.Message}");
|
||||
DiagnosticLog.Error("Gtk4InteropService", $"GTK3 init failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -31,7 +31,7 @@ public static class GtkContextMenuService
|
||||
IntPtr menu = GtkNative.gtk_menu_new();
|
||||
if (menu == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[GtkContextMenuService] Failed to create GTK menu");
|
||||
DiagnosticLog.Error("GtkContextMenuService", "Failed to create GTK menu");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public static class GtkContextMenuService
|
||||
|
||||
ActivateCallback callback = delegate
|
||||
{
|
||||
Console.WriteLine("[GtkContextMenuService] Menu item activated: " + item.Text);
|
||||
DiagnosticLog.Debug("GtkContextMenuService", "Menu item activated: " + item.Text);
|
||||
_actions[actionIndex]?.Invoke();
|
||||
};
|
||||
_callbacks.Add(callback);
|
||||
@@ -88,7 +88,7 @@ public static class GtkContextMenuService
|
||||
GtkNative.gdk_event_free(currentEvent);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GtkContextMenuService] Showed GTK menu with {items.Count} items");
|
||||
DiagnosticLog.Debug("GtkContextMenuService", $"Showed GTK menu with {items.Count} items");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -107,7 +107,7 @@ public static class GtkContextMenuService
|
||||
isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GtkContextMenuService] ApplyMenuTheme: isDark={isDark}");
|
||||
DiagnosticLog.Debug("GtkContextMenuService", $"ApplyMenuTheme: isDark={isDark}");
|
||||
|
||||
// Create comprehensive CSS based on the theme
|
||||
string css = isDark
|
||||
@@ -164,7 +164,7 @@ public static class GtkContextMenuService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkContextMenuService] Error applying menu theme: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkContextMenuService", $"Error applying menu theme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class GtkThemeService
|
||||
isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GtkThemeService] ApplyTheme: isDark={isDark}");
|
||||
DiagnosticLog.Debug("GtkThemeService", $"ApplyTheme: isDark={isDark}");
|
||||
|
||||
// Create comprehensive CSS based on the theme
|
||||
string css = isDark ? GetDarkCss() : GetLightCss();
|
||||
@@ -47,22 +47,30 @@ public static class GtkThemeService
|
||||
IntPtr screen = GtkNative.gdk_screen_get_default();
|
||||
if (screen == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[GtkThemeService] Failed to get default screen");
|
||||
DiagnosticLog.Error("GtkThemeService", "Failed to get default screen");
|
||||
return;
|
||||
}
|
||||
|
||||
// Unreference previous CSS provider to prevent leak
|
||||
if (_currentCssProvider != IntPtr.Zero)
|
||||
{
|
||||
GtkNative.g_object_unref(_currentCssProvider);
|
||||
_currentCssProvider = IntPtr.Zero;
|
||||
}
|
||||
|
||||
// Create new CSS provider
|
||||
IntPtr newProvider = GtkNative.gtk_css_provider_new();
|
||||
if (newProvider == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[GtkThemeService] Failed to create CSS provider");
|
||||
DiagnosticLog.Error("GtkThemeService", "Failed to create CSS provider");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load CSS data
|
||||
if (!GtkNative.gtk_css_provider_load_from_data(newProvider, css, -1, IntPtr.Zero))
|
||||
{
|
||||
Console.WriteLine("[GtkThemeService] Failed to load CSS data");
|
||||
DiagnosticLog.Error("GtkThemeService", "Failed to load CSS data");
|
||||
GtkNative.g_object_unref(newProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,11 +80,11 @@ public static class GtkThemeService
|
||||
// Store reference to current provider
|
||||
_currentCssProvider = newProvider;
|
||||
|
||||
Console.WriteLine("[GtkThemeService] CSS applied successfully");
|
||||
DiagnosticLog.Debug("GtkThemeService", "CSS applied successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GtkThemeService] Error applying theme: {ex.Message}");
|
||||
DiagnosticLog.Error("GtkThemeService", $"Error applying theme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ public class HardwareVideoService : IDisposable
|
||||
{
|
||||
_currentApi = VideoAccelerationApi.VaApi;
|
||||
_initialized = true;
|
||||
Console.WriteLine($"[HardwareVideo] Initialized VA-API with {_supportedProfiles.Count} supported profiles");
|
||||
DiagnosticLog.Debug("HardwareVideoService", $"Initialized VA-API with {_supportedProfiles.Count} supported profiles");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -216,12 +216,12 @@ public class HardwareVideoService : IDisposable
|
||||
{
|
||||
_currentApi = VideoAccelerationApi.Vdpau;
|
||||
_initialized = true;
|
||||
Console.WriteLine("[HardwareVideo] Initialized VDPAU");
|
||||
DiagnosticLog.Debug("HardwareVideoService", "Initialized VDPAU");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("[HardwareVideo] No hardware acceleration available, using software");
|
||||
DiagnosticLog.Warn("HardwareVideoService", "No hardware acceleration available, using software");
|
||||
_currentApi = VideoAccelerationApi.Software;
|
||||
return false;
|
||||
}
|
||||
@@ -261,12 +261,12 @@ public class HardwareVideoService : IDisposable
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Console.WriteLine("[HardwareVideo] VA-API libraries not found");
|
||||
DiagnosticLog.Warn("HardwareVideoService", "VA-API libraries not found");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] VA-API initialization failed: {ex.Message}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"VA-API initialization failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -276,11 +276,11 @@ public class HardwareVideoService : IDisposable
|
||||
int status = vaInitialize(_vaDisplay, out int major, out int minor);
|
||||
if (status != VA_STATUS_SUCCESS)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] vaInitialize failed: {GetVaError(status)}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"vaInitialize failed: {GetVaError(status)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[HardwareVideo] VA-API {major}.{minor} initialized");
|
||||
DiagnosticLog.Debug("HardwareVideoService", $"VA-API {major}.{minor} initialized");
|
||||
|
||||
// Query supported profiles
|
||||
int[] profiles = new int[32];
|
||||
@@ -331,11 +331,11 @@ public class HardwareVideoService : IDisposable
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Console.WriteLine("[HardwareVideo] VDPAU libraries not found");
|
||||
DiagnosticLog.Warn("HardwareVideoService", "VDPAU libraries not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] VDPAU initialization failed: {ex.Message}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"VDPAU initialization failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -355,7 +355,7 @@ public class HardwareVideoService : IDisposable
|
||||
|
||||
if (!_supportedProfiles.Contains(profile))
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] Profile {profile} not supported");
|
||||
DiagnosticLog.Warn("HardwareVideoService", $"Profile {profile} not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ public class HardwareVideoService : IDisposable
|
||||
int status = vaCreateConfig(_vaDisplay, vaProfile, VAEntrypointVLD, IntPtr.Zero, 0, out _vaConfigId);
|
||||
if (status != VA_STATUS_SUCCESS)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] vaCreateConfig failed: {GetVaError(status)}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"vaCreateConfig failed: {GetVaError(status)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ public class HardwareVideoService : IDisposable
|
||||
status = vaCreateSurfaces(_vaDisplay, format, (uint)width, (uint)height, _vaSurfaces, 8, IntPtr.Zero, 0);
|
||||
if (status != VA_STATUS_SUCCESS)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] vaCreateSurfaces failed: {GetVaError(status)}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"vaCreateSurfaces failed: {GetVaError(status)}");
|
||||
vaDestroyConfig(_vaDisplay, _vaConfigId);
|
||||
return false;
|
||||
}
|
||||
@@ -405,13 +405,13 @@ public class HardwareVideoService : IDisposable
|
||||
status = vaCreateContext(_vaDisplay, _vaConfigId, width, height, 0, IntPtr.Zero, 0, out _vaContextId);
|
||||
if (status != VA_STATUS_SUCCESS)
|
||||
{
|
||||
Console.WriteLine($"[HardwareVideo] vaCreateContext failed: {GetVaError(status)}");
|
||||
DiagnosticLog.Error("HardwareVideoService", $"vaCreateContext failed: {GetVaError(status)}");
|
||||
vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length);
|
||||
vaDestroyConfig(_vaDisplay, _vaConfigId);
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[HardwareVideo] Created decoder: {profile} {width}x{height}");
|
||||
DiagnosticLog.Debug("HardwareVideoService", $"Created decoder: {profile} {width}x{height}");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -464,7 +464,7 @@ public class HiDpiService
|
||||
return textScale;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("HiDpiService", "Font scale factor detection failed", ex); }
|
||||
|
||||
return _scaleFactor;
|
||||
}
|
||||
|
||||
@@ -45,14 +45,14 @@ public class IBusInputMethodService : IInputMethodService, IDisposable
|
||||
_bus = ibus_bus_new();
|
||||
if (_bus == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("IBusInputMethodService: Failed to connect to IBus");
|
||||
DiagnosticLog.Error("IBusInputMethodService", "Failed to connect to IBus");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if IBus is connected
|
||||
if (!ibus_bus_is_connected(_bus))
|
||||
{
|
||||
Console.WriteLine("IBusInputMethodService: IBus not connected");
|
||||
DiagnosticLog.Error("IBusInputMethodService", "IBus not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class IBusInputMethodService : IInputMethodService, IDisposable
|
||||
_context = ibus_bus_create_input_context(_bus, "maui-linux");
|
||||
if (_context == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("IBusInputMethodService: Failed to create input context");
|
||||
DiagnosticLog.Error("IBusInputMethodService", "Failed to create input context");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,11 +71,11 @@ public class IBusInputMethodService : IInputMethodService, IDisposable
|
||||
// Connect signals
|
||||
ConnectSignals();
|
||||
|
||||
Console.WriteLine("IBusInputMethodService: Initialized successfully");
|
||||
DiagnosticLog.Debug("IBusInputMethodService", "Initialized successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"IBusInputMethodService: Initialization failed - {ex.Message}");
|
||||
DiagnosticLog.Error("IBusInputMethodService", $"Initialization failed - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,33 +63,33 @@ public static class InputMethodServiceFactory
|
||||
// Try Fcitx5 first if it's the configured IM
|
||||
if (imModule?.Contains("fcitx") == true && Fcitx5InputMethodService.IsAvailable())
|
||||
{
|
||||
Console.WriteLine("InputMethodServiceFactory: Using Fcitx5");
|
||||
DiagnosticLog.Debug("InputMethodServiceFactory", "Using Fcitx5");
|
||||
return CreateFcitx5Service();
|
||||
}
|
||||
|
||||
// Try IBus (most common on modern Linux)
|
||||
if (IsIBusAvailable())
|
||||
{
|
||||
Console.WriteLine("InputMethodServiceFactory: Using IBus");
|
||||
DiagnosticLog.Debug("InputMethodServiceFactory", "Using IBus");
|
||||
return CreateIBusService();
|
||||
}
|
||||
|
||||
// Try Fcitx5 as fallback
|
||||
if (Fcitx5InputMethodService.IsAvailable())
|
||||
{
|
||||
Console.WriteLine("InputMethodServiceFactory: Using Fcitx5");
|
||||
DiagnosticLog.Debug("InputMethodServiceFactory", "Using Fcitx5");
|
||||
return CreateFcitx5Service();
|
||||
}
|
||||
|
||||
// Fall back to XIM
|
||||
if (IsXIMAvailable())
|
||||
{
|
||||
Console.WriteLine("InputMethodServiceFactory: Using XIM");
|
||||
DiagnosticLog.Debug("InputMethodServiceFactory", "Using XIM");
|
||||
return CreateXIMService();
|
||||
}
|
||||
|
||||
// No IME available
|
||||
Console.WriteLine("InputMethodServiceFactory: No IME available, using null service");
|
||||
DiagnosticLog.Warn("InputMethodServiceFactory", "No IME available, using null service");
|
||||
return new NullInputMethodService();
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public static class InputMethodServiceFactory
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"InputMethodServiceFactory: Failed to create IBus service - {ex.Message}");
|
||||
DiagnosticLog.Error("InputMethodServiceFactory", $"Failed to create IBus service - {ex.Message}");
|
||||
return new NullInputMethodService();
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public static class InputMethodServiceFactory
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"InputMethodServiceFactory: Failed to create Fcitx5 service - {ex.Message}");
|
||||
DiagnosticLog.Error("InputMethodServiceFactory", $"Failed to create Fcitx5 service - {ex.Message}");
|
||||
return new NullInputMethodService();
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ public static class InputMethodServiceFactory
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"InputMethodServiceFactory: Failed to create XIM service - {ex.Message}");
|
||||
DiagnosticLog.Error("InputMethodServiceFactory", $"Failed to create XIM service - {ex.Message}");
|
||||
return new NullInputMethodService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class MauiIconGenerator
|
||||
{
|
||||
if (!File.Exists(metaFilePath))
|
||||
{
|
||||
Console.WriteLine("[MauiIconGenerator] Metadata file not found: " + metaFilePath);
|
||||
DiagnosticLog.Error("MauiIconGenerator", "Metadata file not found: " + metaFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ public static class MauiIconGenerator
|
||||
? scaleVal
|
||||
: 0.65f;
|
||||
|
||||
Console.WriteLine($"[MauiIconGenerator] Generating {size}x{size} icon");
|
||||
Console.WriteLine($"[MauiIconGenerator] Color: {color}");
|
||||
Console.WriteLine($"[MauiIconGenerator] Scale: {scale}");
|
||||
DiagnosticLog.Debug("MauiIconGenerator", $"Generating {size}x{size} icon");
|
||||
DiagnosticLog.Debug("MauiIconGenerator", $" Color: {color}");
|
||||
DiagnosticLog.Debug("MauiIconGenerator", $" Scale: {scale}");
|
||||
|
||||
using var surface = SKSurface.Create(new SKImageInfo(size, size, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
var canvas = surface.Canvas;
|
||||
@@ -82,12 +82,12 @@ public static class MauiIconGenerator
|
||||
using var fileStream = File.OpenWrite(outputPath);
|
||||
data.SaveTo(fileStream);
|
||||
|
||||
Console.WriteLine("[MauiIconGenerator] Generated: " + outputPath);
|
||||
DiagnosticLog.Debug("MauiIconGenerator", "Generated: " + outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[MauiIconGenerator] Error: " + ex.Message);
|
||||
DiagnosticLog.Error("MauiIconGenerator", "Error: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class MonitorService : IDisposable
|
||||
_display = X11.XOpenDisplay(IntPtr.Zero);
|
||||
if (_display == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[MonitorService] Failed to open X11 display");
|
||||
DiagnosticLog.Error("MonitorService", "Failed to open X11 display");
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
@@ -117,26 +117,26 @@ public class MonitorService : IDisposable
|
||||
// Check if XRandR is available
|
||||
if (XRandR.XRRQueryExtension(_display, out _eventBase, out _errorBase) == 0)
|
||||
{
|
||||
Console.WriteLine("[MonitorService] XRandR extension not available");
|
||||
DiagnosticLog.Warn("MonitorService", "XRandR extension not available");
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (XRandR.XRRQueryVersion(_display, out int major, out int minor) == 0)
|
||||
{
|
||||
Console.WriteLine("[MonitorService] Failed to query XRandR version");
|
||||
DiagnosticLog.Error("MonitorService", "Failed to query XRandR version");
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[MonitorService] XRandR {major}.{minor} available");
|
||||
DiagnosticLog.Debug("MonitorService", $"XRandR {major}.{minor} available");
|
||||
|
||||
RefreshMonitors();
|
||||
_initialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[MonitorService] Initialization failed: {ex.Message}");
|
||||
DiagnosticLog.Error("MonitorService", $"Initialization failed: {ex.Message}");
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ public class MonitorService : IDisposable
|
||||
resources = XRandR.XRRGetScreenResourcesCurrent(_display, _rootWindow);
|
||||
if (resources == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[MonitorService] Failed to get screen resources");
|
||||
DiagnosticLog.Error("MonitorService", "Failed to get screen resources");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,10 +252,10 @@ public class MonitorService : IDisposable
|
||||
_monitors = newMonitors;
|
||||
|
||||
// Log detected monitors
|
||||
Console.WriteLine($"[MonitorService] Detected {_monitors.Count} monitor(s):");
|
||||
DiagnosticLog.Debug("MonitorService", $"Detected {_monitors.Count} monitor(s):");
|
||||
foreach (var monitor in _monitors)
|
||||
{
|
||||
Console.WriteLine($" {monitor}");
|
||||
DiagnosticLog.Debug("MonitorService", $" {monitor}");
|
||||
}
|
||||
|
||||
// Notify if configuration changed
|
||||
|
||||
@@ -61,7 +61,7 @@ public class NotificationService
|
||||
_dBusMonitor?.Dispose();
|
||||
_dBusMonitor = null;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("NotificationService", "D-Bus monitor cleanup failed", ex); }
|
||||
}
|
||||
|
||||
private async Task MonitorNotificationSignals()
|
||||
@@ -104,7 +104,7 @@ public class NotificationService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationService] D-Bus monitor error: {ex.Message}");
|
||||
DiagnosticLog.Error("NotificationService", $"D-Bus monitor error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class NotificationService
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("NotificationService", "Action invoked processing failed", ex); }
|
||||
}
|
||||
|
||||
private async Task ProcessNotificationClosed(StreamReader reader)
|
||||
@@ -192,7 +192,7 @@ public class NotificationService
|
||||
context?.Tag));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("NotificationService", "Notification closed processing failed", ex); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -270,7 +270,7 @@ public class NotificationService
|
||||
|
||||
_activeNotifications.TryRemove(notificationId, out _);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("NotificationService", "Notification cancel failed", ex); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -91,7 +91,7 @@ public class PortalFilePickerService : IFilePicker
|
||||
else
|
||||
{
|
||||
// No file picker available
|
||||
Console.WriteLine("[FilePickerService] No file picker available (install xdg-desktop-portal, zenity, or kdialog)");
|
||||
DiagnosticLog.Warn("PortalFilePickerService", "No file picker available (install xdg-desktop-portal, zenity, or kdialog)");
|
||||
return Enumerable.Empty<FileResult>();
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class PortalFilePickerService : IFilePicker
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[FilePickerService] Portal error: {ex.Message}");
|
||||
DiagnosticLog.Error("PortalFilePickerService", $"Portal error: {ex.Message}");
|
||||
// Fall back to zenity/kdialog
|
||||
if (_fallbackTool != null)
|
||||
{
|
||||
@@ -358,7 +358,7 @@ public class PortalFilePickerService : IFilePicker
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[FilePickerService] Command error: {ex.Message}");
|
||||
DiagnosticLog.Error("PortalFilePickerService", $"Command error: {ex.Message}");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public class SystemThemeService
|
||||
if (output.ToLowerInvariant().Contains("dark"))
|
||||
return SystemTheme.Dark;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "GNOME theme detection failed", ex); }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public class SystemThemeService
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "KDE theme detection failed", ex); }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -199,7 +199,7 @@ public class SystemThemeService
|
||||
if (output.ToLowerInvariant().Contains("dark"))
|
||||
return SystemTheme.Dark;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "XFCE theme detection failed", ex); }
|
||||
|
||||
return DetectGtkTheme();
|
||||
}
|
||||
@@ -212,7 +212,7 @@ public class SystemThemeService
|
||||
if (output.ToLowerInvariant().Contains("dark"))
|
||||
return SystemTheme.Dark;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "Cinnamon theme detection failed", ex); }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -247,7 +247,7 @@ public class SystemThemeService
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "GTK theme file read failed", ex); }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ public class SystemThemeService
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "KDE accent color parsing failed", ex); }
|
||||
|
||||
return new SKColor(0x21, 0x96, 0xF3);
|
||||
}
|
||||
@@ -373,7 +373,7 @@ public class SystemThemeService
|
||||
_settingsWatcher.Changed += OnSettingsChanged;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemThemeService", "Settings watcher setup failed", ex); }
|
||||
}
|
||||
|
||||
private void SetupPolling()
|
||||
@@ -392,14 +392,14 @@ public class SystemThemeService
|
||||
|
||||
if (oldTheme != CurrentTheme)
|
||||
{
|
||||
Console.WriteLine($"[SystemThemeService] Theme change detected via polling: {oldTheme} -> {CurrentTheme}");
|
||||
DiagnosticLog.Debug("SystemThemeService", $"Theme change detected via polling: {oldTheme} -> {CurrentTheme}");
|
||||
UpdateColors();
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SystemThemeService] Error in poll timer: {ex.Message}");
|
||||
DiagnosticLog.Error("SystemThemeService", $"Error in poll timer: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ public class SystemTrayService : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { DiagnosticLog.Debug("SystemTrayService", "Tray output reading failed", ex); }
|
||||
});
|
||||
|
||||
return Task.FromResult(true);
|
||||
|
||||
@@ -224,7 +224,7 @@ public class VersionTrackingService : IVersionTracking
|
||||
{
|
||||
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||
var version = assembly.GetName().Version;
|
||||
return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : "1.0.0";
|
||||
return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : "9.0.40";
|
||||
}
|
||||
|
||||
private static string GetAssemblyBuild()
|
||||
|
||||
@@ -44,7 +44,7 @@ public class X11InputMethodService : IInputMethodService, IDisposable
|
||||
_display = XOpenDisplay(IntPtr.Zero);
|
||||
if (_display == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("X11InputMethodService: Failed to open display");
|
||||
DiagnosticLog.Error("X11InputMethodService", "Failed to open display");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class X11InputMethodService : IInputMethodService, IDisposable
|
||||
_xim = XOpenIM(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
if (_xim == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("X11InputMethodService: No input method available, trying IBus...");
|
||||
DiagnosticLog.Warn("X11InputMethodService", "No input method available, trying IBus...");
|
||||
TryIBusFallback();
|
||||
return;
|
||||
}
|
||||
@@ -97,7 +97,7 @@ public class X11InputMethodService : IInputMethodService, IDisposable
|
||||
|
||||
if (_xic != IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("X11InputMethodService: Input context created successfully");
|
||||
DiagnosticLog.Debug("X11InputMethodService", "Input context created successfully");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public class X11InputMethodService : IInputMethodService, IDisposable
|
||||
{
|
||||
// Try to connect to IBus via D-Bus
|
||||
// This provides a more modern IME interface
|
||||
Console.WriteLine("X11InputMethodService: IBus fallback not yet implemented");
|
||||
DiagnosticLog.Warn("X11InputMethodService", "IBus fallback not yet implemented");
|
||||
}
|
||||
|
||||
public void SetFocus(IInputContext? context)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -63,10 +64,10 @@ public static class LinuxDialogService
|
||||
|
||||
public static void DrawDialogsOnly(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
Console.WriteLine($"[LinuxDialogService] DrawDialogsOnly: {_activeDialogs.Count} dialogs, IsDarkMode={SkiaTheme.IsDarkMode}");
|
||||
DiagnosticLog.Debug("LinuxDialogService", $"DrawDialogsOnly: {_activeDialogs.Count} dialogs, IsDarkMode={SkiaTheme.IsDarkMode}");
|
||||
foreach (var dialog in _activeDialogs)
|
||||
{
|
||||
Console.WriteLine($"[LinuxDialogService] Drawing dialog: IsVisible={dialog.IsVisible}, Opacity={dialog.Opacity}");
|
||||
DiagnosticLog.Debug("LinuxDialogService", $"Drawing dialog: IsVisible={dialog.IsVisible}, Opacity={dialog.Opacity}");
|
||||
dialog.Measure(new Size(bounds.Width, bounds.Height));
|
||||
dialog.Arrange(new Rect(bounds.Left, bounds.Top, bounds.Width, bounds.Height));
|
||||
dialog.Draw(canvas);
|
||||
@@ -89,7 +90,7 @@ public static class LinuxDialogService
|
||||
|
||||
public static void ShowContextMenu(SkiaContextMenu menu)
|
||||
{
|
||||
Console.WriteLine("[LinuxDialogService] ShowContextMenu called");
|
||||
DiagnosticLog.Debug("LinuxDialogService", "ShowContextMenu called");
|
||||
_activeContextMenu = menu;
|
||||
_showPopupCallback?.Invoke();
|
||||
_invalidateCallback?.Invoke();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Platform.Linux.Interop;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -111,7 +112,7 @@ public class LinuxWebView : SkiaView
|
||||
_gtkWindow = WebKitGtk.gtk_window_new(0);
|
||||
if (_gtkWindow == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[LinuxWebView] Failed to create GTK window");
|
||||
DiagnosticLog.Error("LinuxWebView", "Failed to create GTK window");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@ public class LinuxWebView : SkiaView
|
||||
_webView = WebKitGtk.webkit_web_view_new();
|
||||
if (_webView == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[LinuxWebView] Failed to create WebKit WebView");
|
||||
DiagnosticLog.Error("LinuxWebView", "Failed to create WebKit WebView");
|
||||
WebKitGtk.gtk_widget_destroy(_gtkWindow);
|
||||
_gtkWindow = IntPtr.Zero;
|
||||
return;
|
||||
@@ -148,12 +149,12 @@ public class LinuxWebView : SkiaView
|
||||
WebKitGtk.gtk_container_add(_gtkWindow, _webView);
|
||||
|
||||
_initialized = true;
|
||||
Console.WriteLine("[LinuxWebView] WebKitGTK WebView initialized successfully");
|
||||
DiagnosticLog.Debug("LinuxWebView", "WebKitGTK WebView initialized successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[LinuxWebView] Initialization failed: {ex.Message}");
|
||||
Console.WriteLine($"[LinuxWebView] Make sure WebKitGTK is installed: sudo apt install libwebkit2gtk-4.1-0");
|
||||
DiagnosticLog.Error("LinuxWebView", $"Initialization failed: {ex.Message}", ex);
|
||||
DiagnosticLog.Warn("LinuxWebView", "Make sure WebKitGTK is installed: sudo apt install libwebkit2gtk-4.1-0");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
160
Views/SkiaAbsoluteLayout.cs
Normal file
160
Views/SkiaAbsoluteLayout.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Absolute layout that positions children at exact coordinates.
|
||||
/// </summary>
|
||||
public class SkiaAbsoluteLayout : SkiaLayoutView
|
||||
{
|
||||
private readonly Dictionary<SkiaView, AbsoluteLayoutBounds> _childBounds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child at the specified position and size.
|
||||
/// </summary>
|
||||
public void AddChild(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
|
||||
{
|
||||
base.AddChild(child);
|
||||
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
|
||||
}
|
||||
|
||||
public override void RemoveChild(SkiaView child)
|
||||
{
|
||||
base.RemoveChild(child);
|
||||
_childBounds.Remove(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layout bounds for a child.
|
||||
/// </summary>
|
||||
public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child)
|
||||
{
|
||||
return _childBounds.TryGetValue(child, out var bounds)
|
||||
? bounds
|
||||
: new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the layout bounds for a child.
|
||||
/// </summary>
|
||||
public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
|
||||
{
|
||||
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
float maxRight = 0;
|
||||
float maxBottom = 0;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var layout = GetLayoutBounds(child);
|
||||
var bounds = layout.Bounds;
|
||||
|
||||
child.Measure(new Size(bounds.Width, bounds.Height));
|
||||
|
||||
maxRight = Math.Max(maxRight, bounds.Right);
|
||||
maxBottom = Math.Max(maxBottom, bounds.Bottom);
|
||||
}
|
||||
|
||||
return new Size(
|
||||
maxRight + Padding.Left + Padding.Right,
|
||||
maxBottom + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
|
||||
protected override Rect ArrangeOverride(Rect bounds)
|
||||
{
|
||||
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var layout = GetLayoutBounds(child);
|
||||
var childBounds = layout.Bounds;
|
||||
var flags = layout.Flags;
|
||||
|
||||
float x, y, width, height;
|
||||
|
||||
// X position
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.XProportional))
|
||||
x = content.Left + childBounds.Left * content.Width;
|
||||
else
|
||||
x = content.Left + childBounds.Left;
|
||||
|
||||
// Y position
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.YProportional))
|
||||
y = content.Top + childBounds.Top * content.Height;
|
||||
else
|
||||
y = content.Top + childBounds.Top;
|
||||
|
||||
// Width
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.WidthProportional))
|
||||
width = childBounds.Width * content.Width;
|
||||
else if (childBounds.Width < 0)
|
||||
width = (float)child.DesiredSize.Width;
|
||||
else
|
||||
width = childBounds.Width;
|
||||
|
||||
// Height
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional))
|
||||
height = childBounds.Height * content.Height;
|
||||
else if (childBounds.Height < 0)
|
||||
height = (float)child.DesiredSize.Height;
|
||||
else
|
||||
height = childBounds.Height;
|
||||
|
||||
// Apply child's margin
|
||||
var margin = child.Margin;
|
||||
var marginedBounds = new Rect(
|
||||
x + (float)margin.Left,
|
||||
y + (float)margin.Top,
|
||||
width - (float)margin.Left - (float)margin.Right,
|
||||
height - (float)margin.Top - (float)margin.Bottom);
|
||||
child.Arrange(marginedBounds);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute layout bounds for a child.
|
||||
/// </summary>
|
||||
public readonly struct AbsoluteLayoutBounds
|
||||
{
|
||||
public SKRect Bounds { get; }
|
||||
public AbsoluteLayoutFlags Flags { get; }
|
||||
|
||||
public AbsoluteLayoutBounds(SKRect bounds, AbsoluteLayoutFlags flags)
|
||||
{
|
||||
Bounds = bounds;
|
||||
Flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags for absolute layout positioning.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum AbsoluteLayoutFlags
|
||||
{
|
||||
None = 0,
|
||||
XProportional = 1,
|
||||
YProportional = 2,
|
||||
WidthProportional = 4,
|
||||
HeightProportional = 8,
|
||||
PositionProportional = XProportional | YProportional,
|
||||
SizeProportional = WidthProportional | HeightProportional,
|
||||
All = XProportional | YProportional | WidthProportional | HeightProportional
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -62,7 +63,7 @@ public class SkiaAlertDialog : SkiaView
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var app = Application.Current;
|
||||
Console.WriteLine($"[SkiaAlertDialog] OnDraw: app={app != null}, UserAppTheme={app?.UserAppTheme}, RequestedTheme={app?.RequestedTheme}, IsDarkMode={SkiaTheme.IsDarkMode}, DialogBg={DialogBackground}");
|
||||
DiagnosticLog.Debug("SkiaAlertDialog", $"OnDraw: app={app != null}, UserAppTheme={app?.UserAppTheme}, RequestedTheme={app?.RequestedTheme}, IsDarkMode={SkiaTheme.IsDarkMode}, DialogBg={DialogBackground}");
|
||||
|
||||
// Draw semi-transparent overlay covering entire screen
|
||||
using var overlayPaint = new SKPaint
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Windows.Input;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -1096,7 +1097,7 @@ public class SkiaButton : SkiaView, IButtonController
|
||||
|
||||
var result = new Size(Math.Max(width, 44f), Math.Max(height, 36f));
|
||||
if (Text == "Round")
|
||||
Console.WriteLine($"[SkiaButton.Measure] Text='Round' WReq={WidthRequest} HReq={HeightRequest} width={width:F1} height={height:F1} result={result.Width:F0}x{result.Height:F0}");
|
||||
DiagnosticLog.Debug("SkiaButton", $"Measure Text='Round' WReq={WidthRequest} HReq={HeightRequest} width={width:F1} height={height:F1} result={result.Width:F0}x{result.Height:F0}");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -460,7 +461,7 @@ public class SkiaCollectionView : SkiaItemsView
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[SkiaCollectionView.DrawItem] EXCEPTION: " + ex.Message + "\n" + ex.StackTrace);
|
||||
DiagnosticLog.Error("SkiaCollectionView", "DrawItem EXCEPTION: " + ex.Message + "\n" + ex.StackTrace, ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
255
Views/SkiaEditor.Drawing.cs
Normal file
255
Views/SkiaEditor.Drawing.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 System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public partial class SkiaEditor
|
||||
{
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var paddingLeft = (float)Padding.Left;
|
||||
var paddingTop = (float)Padding.Top;
|
||||
var paddingRight = (float)Padding.Right;
|
||||
var paddingBottom = (float)Padding.Bottom;
|
||||
var fontSize = (float)FontSize;
|
||||
var lineHeight = (float)LineHeight;
|
||||
var cornerRadius = (float)CornerRadius;
|
||||
|
||||
// Update wrap width if bounds changed and re-wrap text
|
||||
var newWrapWidth = bounds.Width - paddingLeft - paddingRight;
|
||||
if (Math.Abs(newWrapWidth - _wrapWidth) > 1)
|
||||
{
|
||||
_wrapWidth = newWrapWidth;
|
||||
UpdateLines();
|
||||
}
|
||||
|
||||
// Draw background
|
||||
var bgColor = EditorBackgroundColor != null ? ToSKColor(EditorBackgroundColor) :
|
||||
(IsEnabled ? SkiaTheme.BackgroundWhiteSK : SkiaTheme.Gray100SK);
|
||||
using var bgPaint = new SKPaint
|
||||
{
|
||||
Color = bgColor,
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
|
||||
|
||||
// Draw border only if BorderColor is not transparent
|
||||
if (BorderColor != null && BorderColor != Colors.Transparent && BorderColor.Alpha > 0)
|
||||
{
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = IsFocused ? 2 : 1,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
|
||||
}
|
||||
|
||||
// Setup text rendering
|
||||
using var font = new SKFont(SKTypeface.Default, fontSize);
|
||||
var lineSpacing = fontSize * lineHeight;
|
||||
|
||||
// Clip to content area
|
||||
var contentRect = new SKRect(
|
||||
bounds.Left + paddingLeft,
|
||||
bounds.Top + paddingTop,
|
||||
bounds.Right - paddingRight,
|
||||
bounds.Bottom - paddingBottom);
|
||||
|
||||
canvas.Save();
|
||||
canvas.ClipRect(contentRect);
|
||||
// Don't translate - let the text draw at absolute positions
|
||||
// canvas.Translate(0, -_scrollOffsetY);
|
||||
|
||||
if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder))
|
||||
{
|
||||
using var placeholderPaint = new SKPaint(font)
|
||||
{
|
||||
Color = GetEffectivePlaceholderColor(),
|
||||
IsAntialias = true
|
||||
};
|
||||
// Handle multiline placeholder text by splitting on newlines
|
||||
var placeholderLines = Placeholder.Split('\n');
|
||||
var y = contentRect.Top + fontSize;
|
||||
foreach (var line in placeholderLines)
|
||||
{
|
||||
canvas.DrawText(line, contentRect.Left, y, placeholderPaint);
|
||||
y += lineSpacing;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var textColor = GetEffectiveTextColor();
|
||||
using var textPaint = new SKPaint(font)
|
||||
{
|
||||
Color = IsEnabled ? textColor : textColor.WithAlpha(128),
|
||||
IsAntialias = true
|
||||
};
|
||||
using var selectionPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(SelectionColor),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
var y = contentRect.Top + fontSize;
|
||||
var charIndex = 0;
|
||||
|
||||
for (int lineIndex = 0; lineIndex < _lines.Count; lineIndex++)
|
||||
{
|
||||
var line = _lines[lineIndex];
|
||||
var x = contentRect.Left;
|
||||
|
||||
// Draw selection for this line if applicable
|
||||
if (_selectionStart >= 0 && _selectionLength != 0)
|
||||
{
|
||||
// Handle both positive and negative selection lengths
|
||||
var selStart = _selectionLength > 0 ? _selectionStart : _selectionStart + _selectionLength;
|
||||
var selEnd = _selectionLength > 0 ? _selectionStart + _selectionLength : _selectionStart;
|
||||
var lineStart = charIndex;
|
||||
var lineEnd = charIndex + line.Length;
|
||||
|
||||
if (selEnd > lineStart && selStart < lineEnd)
|
||||
{
|
||||
var selStartInLine = Math.Max(0, selStart - lineStart);
|
||||
var selEndInLine = Math.Min(line.Length, selEnd - lineStart);
|
||||
|
||||
var startX = x + MeasureText(line.Substring(0, selStartInLine), font);
|
||||
var endX = x + MeasureText(line.Substring(0, selEndInLine), font);
|
||||
|
||||
canvas.DrawRect(new SKRect(startX, y - fontSize, endX, y + lineSpacing - fontSize), selectionPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if pre-edit text should be displayed on this line
|
||||
var (cursorLine, cursorCol) = GetLineColumn(_cursorPosition);
|
||||
var displayLine = line;
|
||||
var hasPreEditOnThisLine = !string.IsNullOrEmpty(_preEditText) && cursorLine == lineIndex;
|
||||
|
||||
if (hasPreEditOnThisLine)
|
||||
{
|
||||
// Insert pre-edit text at cursor position within this line
|
||||
var insertPos = Math.Min(cursorCol, line.Length);
|
||||
displayLine = line.Insert(insertPos, _preEditText);
|
||||
}
|
||||
|
||||
// Draw the text with font fallback for emoji/CJK support
|
||||
DrawTextWithFallback(canvas, displayLine, x, y, textPaint, SKTypeface.Default);
|
||||
|
||||
// Draw underline for pre-edit (composition) text
|
||||
if (hasPreEditOnThisLine)
|
||||
{
|
||||
DrawPreEditUnderline(canvas, textPaint, line, x, y, contentRect);
|
||||
}
|
||||
|
||||
// Draw cursor if on this line
|
||||
if (IsFocused && _cursorVisible)
|
||||
{
|
||||
if (cursorLine == lineIndex)
|
||||
{
|
||||
// Account for pre-edit text when calculating cursor position
|
||||
var textToCursor = line.Substring(0, Math.Min(cursorCol, line.Length));
|
||||
var cursorX = x + MeasureText(textToCursor, font);
|
||||
|
||||
// If there's pre-edit text, cursor goes after it
|
||||
if (hasPreEditOnThisLine && _preEditText.Length > 0)
|
||||
{
|
||||
cursorX += MeasureText(_preEditText, font);
|
||||
}
|
||||
|
||||
using var cursorPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(CursorColor),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 2,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawLine(cursorX, y - fontSize + 2, cursorX, y + 2, cursorPaint);
|
||||
}
|
||||
}
|
||||
|
||||
y += lineSpacing;
|
||||
charIndex += line.Length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.Restore();
|
||||
|
||||
// Draw scrollbar if needed
|
||||
var totalHeight = _lines.Count * fontSize * lineHeight;
|
||||
if (totalHeight > contentRect.Height)
|
||||
{
|
||||
DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private float MeasureText(string text, SKFont font)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return 0;
|
||||
using var paint = new SKPaint(font);
|
||||
return paint.MeasureText(text);
|
||||
}
|
||||
|
||||
private void DrawScrollbar(SKCanvas canvas, SKRect bounds, float viewHeight, float contentHeight)
|
||||
{
|
||||
var scrollbarWidth = 6f;
|
||||
var scrollbarMargin = 2f;
|
||||
var paddingTop = (float)Padding.Top;
|
||||
var scrollbarHeight = Math.Max(20, viewHeight * (viewHeight / contentHeight));
|
||||
var scrollbarY = bounds.Top + paddingTop + (_scrollOffsetY / contentHeight) * (viewHeight - scrollbarHeight);
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = SkiaTheme.Shadow25SK,
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
canvas.DrawRoundRect(new SKRoundRect(
|
||||
new SKRect(
|
||||
bounds.Right - scrollbarWidth - scrollbarMargin,
|
||||
scrollbarY,
|
||||
bounds.Right - scrollbarMargin,
|
||||
scrollbarY + scrollbarHeight),
|
||||
scrollbarWidth / 2), paint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws text with font fallback for emoji, CJK, and other scripts.
|
||||
/// </summary>
|
||||
private void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface)
|
||||
=> TextRenderingHelper.DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface, (float)FontSize);
|
||||
|
||||
/// <summary>
|
||||
/// Draws underline for IME pre-edit (composition) text.
|
||||
/// </summary>
|
||||
private void DrawPreEditUnderline(SKCanvas canvas, SKPaint paint, string displayText, float x, float y, SKRect bounds)
|
||||
=> TextRenderingHelper.DrawPreEditUnderline(canvas, paint, displayText, _cursorPosition, _preEditText, x, y);
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
if (AutoSize)
|
||||
{
|
||||
var fontSize = (float)FontSize;
|
||||
var lineHeight = (float)LineHeight;
|
||||
var lineSpacing = fontSize * lineHeight;
|
||||
var verticalPadding = Padding.Top + Padding.Bottom;
|
||||
var height = Math.Max(lineSpacing + verticalPadding, _lines.Count * lineSpacing + verticalPadding);
|
||||
return new Size(
|
||||
availableSize.Width < double.MaxValue ? availableSize.Width : 200,
|
||||
Math.Min(height, availableSize.Height < double.MaxValue ? availableSize.Height : 200));
|
||||
}
|
||||
|
||||
return new Size(
|
||||
availableSize.Width < double.MaxValue ? Math.Min(availableSize.Width, 200) : 200,
|
||||
availableSize.Height < double.MaxValue ? Math.Min(availableSize.Height, 150) : 150);
|
||||
}
|
||||
}
|
||||
756
Views/SkiaEditor.Input.cs
Normal file
756
Views/SkiaEditor.Input.cs
Normal file
@@ -0,0 +1,756 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public partial class SkiaEditor
|
||||
{
|
||||
#region IInputContext Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text for IME context.
|
||||
/// </summary>
|
||||
string IInputContext.Text
|
||||
{
|
||||
get => Text;
|
||||
set => Text = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor position for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.CursorPosition
|
||||
{
|
||||
get => _cursorPosition;
|
||||
set => CursorPosition = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selection start for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.SelectionStart => _selectionStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selection length for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.SelectionLength => _selectionLength;
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME commits text.
|
||||
/// </summary>
|
||||
public void OnTextCommitted(string text)
|
||||
{
|
||||
if (IsReadOnly) return;
|
||||
|
||||
// Delete selection if any
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
|
||||
// Clear pre-edit text
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
|
||||
// Check max length
|
||||
if (MaxLength > 0 && Text.Length + text.Length > MaxLength)
|
||||
{
|
||||
text = text.Substring(0, MaxLength - Text.Length);
|
||||
}
|
||||
|
||||
// Insert committed text at cursor
|
||||
var newText = Text.Insert(_cursorPosition, text);
|
||||
var newPos = _cursorPosition + text.Length;
|
||||
Text = newText;
|
||||
_cursorPosition = newPos;
|
||||
|
||||
EnsureCursorVisible();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME pre-edit (composition) text changes.
|
||||
/// </summary>
|
||||
public void OnPreEditChanged(string preEditText, int cursorPosition)
|
||||
{
|
||||
_preEditText = preEditText ?? string.Empty;
|
||||
_preEditCursorPosition = cursorPosition;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME pre-edit ends (cancelled or committed).
|
||||
/// </summary>
|
||||
public void OnPreEditEnded()
|
||||
{
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void UpdateLines()
|
||||
{
|
||||
_lines.Clear();
|
||||
var text = Text ?? "";
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
_lines.Add("");
|
||||
return;
|
||||
}
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, (float)FontSize);
|
||||
|
||||
// Split by actual newlines first
|
||||
var paragraphs = text.Split('\n');
|
||||
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(paragraph))
|
||||
{
|
||||
_lines.Add("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Word wrap this paragraph if we have a known width
|
||||
if (_wrapWidth > 0)
|
||||
{
|
||||
WrapParagraph(paragraph, font, _wrapWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lines.Add(paragraph);
|
||||
}
|
||||
}
|
||||
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
_lines.Add("");
|
||||
}
|
||||
}
|
||||
|
||||
private void WrapParagraph(string paragraph, SKFont font, float maxWidth)
|
||||
{
|
||||
var words = paragraph.Split(' ');
|
||||
var currentLine = "";
|
||||
|
||||
foreach (var word in words)
|
||||
{
|
||||
var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word;
|
||||
var lineWidth = MeasureText(testLine, font);
|
||||
|
||||
if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine))
|
||||
{
|
||||
// Line too long, save current and start new
|
||||
_lines.Add(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (!string.IsNullOrEmpty(currentLine))
|
||||
{
|
||||
_lines.Add(currentLine);
|
||||
}
|
||||
}
|
||||
|
||||
private (int line, int column) GetLineColumn(int position)
|
||||
{
|
||||
var pos = 0;
|
||||
for (int i = 0; i < _lines.Count; i++)
|
||||
{
|
||||
var lineLength = _lines[i].Length;
|
||||
if (pos + lineLength >= position || i == _lines.Count - 1)
|
||||
{
|
||||
return (i, position - pos);
|
||||
}
|
||||
pos += lineLength + 1;
|
||||
}
|
||||
return (_lines.Count - 1, _lines[^1].Length);
|
||||
}
|
||||
|
||||
private int GetPosition(int line, int column)
|
||||
{
|
||||
var pos = 0;
|
||||
for (int i = 0; i < line && i < _lines.Count; i++)
|
||||
{
|
||||
pos += _lines[i].Length + 1;
|
||||
}
|
||||
if (line < _lines.Count)
|
||||
{
|
||||
pos += Math.Min(column, _lines[line].Length);
|
||||
}
|
||||
return Math.Min(pos, Text.Length);
|
||||
}
|
||||
|
||||
private void EnsureCursorVisible()
|
||||
{
|
||||
var (line, col) = GetLineColumn(_cursorPosition);
|
||||
var fontSize = (float)FontSize;
|
||||
var lineHeight = (float)LineHeight;
|
||||
var lineSpacing = fontSize * lineHeight;
|
||||
var cursorY = line * lineSpacing;
|
||||
var viewHeight = Bounds.Height - (float)(Padding.Top + Padding.Bottom);
|
||||
|
||||
if (cursorY < _scrollOffsetY)
|
||||
{
|
||||
_scrollOffsetY = cursorY;
|
||||
}
|
||||
else if (cursorY + lineSpacing > _scrollOffsetY + (float)viewHeight)
|
||||
{
|
||||
_scrollOffsetY = cursorY + lineSpacing - (float)viewHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
DiagnosticLog.Debug("SkiaEditor", $"OnPointerPressed: Button={e.Button}, IsEnabled={IsEnabled}");
|
||||
if (!IsEnabled) return;
|
||||
|
||||
// Handle right-click context menu
|
||||
if (e.Button == PointerButton.Right)
|
||||
{
|
||||
DiagnosticLog.Debug("SkiaEditor", "Right-click detected, showing context menu");
|
||||
ShowContextMenu(e.X, e.Y);
|
||||
return;
|
||||
}
|
||||
|
||||
IsFocused = true;
|
||||
|
||||
// Use screen coordinates for proper hit detection
|
||||
var screenBounds = ScreenBounds;
|
||||
var paddingLeft = (float)Padding.Left;
|
||||
var paddingTop = (float)Padding.Top;
|
||||
var contentX = e.X - screenBounds.Left - paddingLeft;
|
||||
var contentY = e.Y - screenBounds.Top - paddingTop + _scrollOffsetY;
|
||||
|
||||
var fontSize = (float)FontSize;
|
||||
var lineSpacing = fontSize * (float)LineHeight;
|
||||
var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1);
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, fontSize);
|
||||
var line = _lines[clickedLine];
|
||||
var clickedCol = 0;
|
||||
|
||||
for (int i = 0; i <= line.Length; i++)
|
||||
{
|
||||
var charX = MeasureText(line.Substring(0, i), font);
|
||||
if (charX > contentX)
|
||||
{
|
||||
clickedCol = i > 0 ? i - 1 : 0;
|
||||
break;
|
||||
}
|
||||
clickedCol = i;
|
||||
}
|
||||
|
||||
_cursorPosition = GetPosition(clickedLine, clickedCol);
|
||||
|
||||
// Check for double-click (select word)
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||
var distanceFromLastClick = Math.Sqrt(Math.Pow(e.X - _lastClickX, 2) + Math.Pow(e.Y - _lastClickY, 2));
|
||||
|
||||
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
|
||||
{
|
||||
// Double-click: select the word at cursor
|
||||
SelectWordAtCursor();
|
||||
_lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues
|
||||
_isSelecting = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single click: start selection
|
||||
_selectionStart = _cursorPosition;
|
||||
_selectionLength = 0;
|
||||
_isSelecting = true;
|
||||
_lastClickTime = now;
|
||||
_lastClickX = e.X;
|
||||
_lastClickY = e.Y;
|
||||
}
|
||||
|
||||
_cursorVisible = true;
|
||||
_lastCursorBlink = DateTime.Now;
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
if (!IsEnabled || !_isSelecting) return;
|
||||
|
||||
// Calculate position from mouse coordinates
|
||||
var screenBounds = ScreenBounds;
|
||||
var paddingLeft = (float)Padding.Left;
|
||||
var paddingTop = (float)Padding.Top;
|
||||
var contentX = e.X - screenBounds.Left - paddingLeft;
|
||||
var contentY = e.Y - screenBounds.Top - paddingTop + _scrollOffsetY;
|
||||
|
||||
var fontSize = (float)FontSize;
|
||||
var lineSpacing = fontSize * (float)LineHeight;
|
||||
var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1);
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, fontSize);
|
||||
var line = _lines[clickedLine];
|
||||
var clickedCol = 0;
|
||||
|
||||
for (int i = 0; i <= line.Length; i++)
|
||||
{
|
||||
var charX = MeasureText(line.Substring(0, i), font);
|
||||
if (charX > contentX)
|
||||
{
|
||||
clickedCol = i > 0 ? i - 1 : 0;
|
||||
break;
|
||||
}
|
||||
clickedCol = i;
|
||||
}
|
||||
|
||||
var newPosition = GetPosition(clickedLine, clickedCol);
|
||||
if (newPosition != _cursorPosition)
|
||||
{
|
||||
_cursorPosition = newPosition;
|
||||
_selectionLength = _cursorPosition - _selectionStart;
|
||||
_cursorVisible = true;
|
||||
_lastCursorBlink = DateTime.Now;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerReleased(PointerEventArgs e)
|
||||
{
|
||||
_isSelecting = false;
|
||||
}
|
||||
|
||||
public override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
var (line, col) = GetLineColumn(_cursorPosition);
|
||||
_cursorVisible = true;
|
||||
_lastCursorBlink = DateTime.Now;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
if (_cursorPosition > 0)
|
||||
{
|
||||
_cursorPosition--;
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
if (_cursorPosition < Text.Length)
|
||||
{
|
||||
_cursorPosition++;
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Up:
|
||||
if (line > 0)
|
||||
{
|
||||
_cursorPosition = GetPosition(line - 1, col);
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
if (line < _lines.Count - 1)
|
||||
{
|
||||
_cursorPosition = GetPosition(line + 1, col);
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Home:
|
||||
_cursorPosition = GetPosition(line, 0);
|
||||
EnsureCursorVisible();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.End:
|
||||
_cursorPosition = GetPosition(line, _lines[line].Length);
|
||||
EnsureCursorVisible();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
InsertText("\n");
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Backspace:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
else if (_cursorPosition > 0)
|
||||
{
|
||||
Text = Text.Remove(_cursorPosition - 1, 1);
|
||||
_cursorPosition--;
|
||||
}
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Delete:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
else if (_cursorPosition < Text.Length)
|
||||
{
|
||||
Text = Text.Remove(_cursorPosition, 1);
|
||||
}
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Tab:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
InsertText(" ");
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.A:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.C:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
CopyToClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.V:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly)
|
||||
{
|
||||
PasteFromClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.X:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly)
|
||||
{
|
||||
CutToClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public override void OnTextInput(TextInputEventArgs e)
|
||||
{
|
||||
if (!IsEnabled || IsReadOnly) return;
|
||||
|
||||
// Ignore control characters (Ctrl+key combinations send ASCII control codes)
|
||||
if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(e.Text))
|
||||
{
|
||||
InsertText(e.Text);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertText(string text)
|
||||
{
|
||||
if (_selectionLength > 0)
|
||||
{
|
||||
var currentText = Text;
|
||||
Text = currentText.Remove(_selectionStart, _selectionLength);
|
||||
_cursorPosition = _selectionStart;
|
||||
_selectionStart = -1;
|
||||
_selectionLength = 0;
|
||||
}
|
||||
|
||||
if (MaxLength > 0 && Text.Length + text.Length > MaxLength)
|
||||
{
|
||||
text = text.Substring(0, Math.Max(0, MaxLength - Text.Length));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
Text = Text.Insert(_cursorPosition, text);
|
||||
_cursorPosition += text.Length;
|
||||
EnsureCursorVisible();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnScroll(ScrollEventArgs e)
|
||||
{
|
||||
var fontSize = (float)FontSize;
|
||||
var lineHeight = (float)LineHeight;
|
||||
var lineSpacing = fontSize * lineHeight;
|
||||
var totalHeight = _lines.Count * lineSpacing;
|
||||
var viewHeight = (float)Bounds.Height - (float)(Padding.Top + Padding.Bottom);
|
||||
var maxScroll = Math.Max(0, totalHeight - viewHeight);
|
||||
|
||||
_scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3, 0, maxScroll);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public override void OnFocusGained()
|
||||
{
|
||||
base.OnFocusGained();
|
||||
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused);
|
||||
|
||||
// Connect to IME service
|
||||
_inputMethodService?.SetFocus(this);
|
||||
|
||||
// Update cursor location for IME candidate window positioning
|
||||
UpdateImeCursorLocation();
|
||||
}
|
||||
|
||||
public override void OnFocusLost()
|
||||
{
|
||||
base.OnFocusLost();
|
||||
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal);
|
||||
|
||||
// Disconnect from IME service and reset any composition
|
||||
_inputMethodService?.SetFocus(null);
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the cursor blink timer (shows cursor immediately).
|
||||
/// </summary>
|
||||
private void ResetCursorBlink()
|
||||
{
|
||||
_lastCursorBlink = DateTime.Now;
|
||||
_cursorVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates cursor blink animation. Called by the application's animation loop.
|
||||
/// </summary>
|
||||
public void UpdateCursorBlink()
|
||||
{
|
||||
if (!IsFocused) return;
|
||||
|
||||
var elapsed = (DateTime.Now - _lastCursorBlink).TotalMilliseconds;
|
||||
var newVisible = ((int)(elapsed / 500) % 2) == 0;
|
||||
|
||||
if (newVisible != _cursorVisible)
|
||||
{
|
||||
_cursorVisible = newVisible;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
#region Selection and Clipboard
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
_selectionStart = 0;
|
||||
_cursorPosition = Text.Length;
|
||||
_selectionLength = Text.Length;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void SelectWordAtCursor()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text)) return;
|
||||
|
||||
// Find word boundaries
|
||||
int start = _cursorPosition;
|
||||
int end = _cursorPosition;
|
||||
|
||||
// Move start backwards to beginning of word
|
||||
while (start > 0 && IsWordChar(Text[start - 1]))
|
||||
start--;
|
||||
|
||||
// Move end forwards to end of word
|
||||
while (end < Text.Length && IsWordChar(Text[end]))
|
||||
end++;
|
||||
|
||||
_selectionStart = start;
|
||||
_cursorPosition = end;
|
||||
_selectionLength = end - start;
|
||||
}
|
||||
|
||||
private static bool IsWordChar(char c)
|
||||
{
|
||||
return char.IsLetterOrDigit(c) || c == '_';
|
||||
}
|
||||
|
||||
private void CopyToClipboard()
|
||||
{
|
||||
if (_selectionLength == 0) return;
|
||||
|
||||
var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
|
||||
var length = Math.Abs(_selectionLength);
|
||||
var selectedText = Text.Substring(start, length);
|
||||
|
||||
// Use system clipboard via xclip/xsel
|
||||
SystemClipboard.SetText(selectedText);
|
||||
}
|
||||
|
||||
private void CutToClipboard()
|
||||
{
|
||||
CopyToClipboard();
|
||||
DeleteSelection();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void PasteFromClipboard()
|
||||
{
|
||||
// Get from system clipboard
|
||||
var text = SystemClipboard.GetText();
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
|
||||
InsertText(text);
|
||||
}
|
||||
|
||||
private void DeleteSelection()
|
||||
{
|
||||
if (_selectionLength == 0) return;
|
||||
|
||||
var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
|
||||
var length = Math.Abs(_selectionLength);
|
||||
|
||||
Text = Text.Remove(start, length);
|
||||
_cursorPosition = start;
|
||||
_selectionStart = -1;
|
||||
_selectionLength = 0;
|
||||
}
|
||||
|
||||
private void ShowContextMenu(float x, float y)
|
||||
{
|
||||
DiagnosticLog.Debug("SkiaEditor", $"ShowContextMenu at ({x}, {y}), IsGtkMode={LinuxApplication.IsGtkMode}");
|
||||
bool hasSelection = _selectionLength != 0;
|
||||
bool hasText = !string.IsNullOrEmpty(Text);
|
||||
bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText());
|
||||
bool isEditable = !IsReadOnly;
|
||||
|
||||
if (LinuxApplication.IsGtkMode)
|
||||
{
|
||||
// Use GTK context menu when running in GTK mode (e.g., with WebView)
|
||||
GtkContextMenuService.ShowContextMenu(new List<GtkMenuItem>
|
||||
{
|
||||
new GtkMenuItem("Cut", () =>
|
||||
{
|
||||
CutToClipboard();
|
||||
Invalidate();
|
||||
}, hasSelection && isEditable),
|
||||
new GtkMenuItem("Copy", () =>
|
||||
{
|
||||
CopyToClipboard();
|
||||
}, hasSelection),
|
||||
new GtkMenuItem("Paste", () =>
|
||||
{
|
||||
PasteFromClipboard();
|
||||
Invalidate();
|
||||
}, hasClipboard && isEditable),
|
||||
GtkMenuItem.Separator,
|
||||
new GtkMenuItem("Select All", () =>
|
||||
{
|
||||
SelectAll();
|
||||
Invalidate();
|
||||
}, hasText)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use Skia-rendered context menu for pure Skia mode (Wayland/X11)
|
||||
bool isDarkTheme = Application.Current?.RequestedTheme == AppTheme.Dark;
|
||||
var items = new List<ContextMenuItem>
|
||||
{
|
||||
new ContextMenuItem("Cut", () =>
|
||||
{
|
||||
CutToClipboard();
|
||||
Invalidate();
|
||||
}, hasSelection && isEditable),
|
||||
new ContextMenuItem("Copy", () =>
|
||||
{
|
||||
CopyToClipboard();
|
||||
}, hasSelection),
|
||||
new ContextMenuItem("Paste", () =>
|
||||
{
|
||||
PasteFromClipboard();
|
||||
Invalidate();
|
||||
}, hasClipboard && isEditable),
|
||||
ContextMenuItem.Separator,
|
||||
new ContextMenuItem("Select All", () =>
|
||||
{
|
||||
SelectAll();
|
||||
Invalidate();
|
||||
}, hasText)
|
||||
};
|
||||
var menu = new SkiaContextMenu(x, y, items, isDarkTheme);
|
||||
LinuxDialogService.ShowContextMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Updates the IME cursor location for candidate window positioning.
|
||||
/// </summary>
|
||||
private void UpdateImeCursorLocation()
|
||||
{
|
||||
if (_inputMethodService == null) return;
|
||||
|
||||
var screenBounds = ScreenBounds;
|
||||
var fontSize = (float)FontSize;
|
||||
var lineSpacing = fontSize * (float)LineHeight;
|
||||
var (line, col) = GetLineColumn(_cursorPosition);
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, fontSize);
|
||||
var lineText = line < _lines.Count ? _lines[line] : "";
|
||||
var textToCursor = lineText.Substring(0, Math.Min(col, lineText.Length));
|
||||
var cursorX = MeasureText(textToCursor, font);
|
||||
|
||||
int x = (int)(screenBounds.Left + Padding.Left + cursorX);
|
||||
int y = (int)(screenBounds.Top + Padding.Top + line * lineSpacing - _scrollOffsetY);
|
||||
int height = (int)fontSize;
|
||||
|
||||
_inputMethodService.SetCursorLocation(x, y, 2, height);
|
||||
}
|
||||
}
|
||||
1043
Views/SkiaEditor.cs
1043
Views/SkiaEditor.cs
File diff suppressed because it is too large
Load Diff
303
Views/SkiaEntry.Drawing.cs
Normal file
303
Views/SkiaEntry.Drawing.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public partial class SkiaEntry
|
||||
{
|
||||
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
// Skip base background drawing if Entry is transparent
|
||||
// (transparent Entry is likely inside a Border that handles appearance)
|
||||
var bgColor = ToSKColor(EntryBackgroundColor);
|
||||
var baseBgColor = GetEffectiveBackgroundColor();
|
||||
if (bgColor.Alpha < 10 && baseBgColor.Alpha < 10)
|
||||
return;
|
||||
|
||||
// Otherwise let base class draw
|
||||
base.DrawBackground(canvas, bounds);
|
||||
}
|
||||
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var bgColor = ToSKColor(EntryBackgroundColor);
|
||||
var isTransparent = bgColor.Alpha < 10; // Consider nearly transparent as transparent
|
||||
|
||||
if (!isTransparent)
|
||||
{
|
||||
// Draw background
|
||||
using var bgPaint = new SKPaint
|
||||
{
|
||||
Color = bgColor,
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
var rect = new SKRoundRect(bounds, (float)CornerRadius);
|
||||
canvas.DrawRoundRect(rect, bgPaint);
|
||||
|
||||
// Draw border
|
||||
var borderColor = IsFocused ? ToSKColor(FocusedBorderColor) : ToSKColor(BorderColor);
|
||||
var borderWidth = IsFocused ? (float)BorderWidth + 1 : (float)BorderWidth;
|
||||
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = borderColor,
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = borderWidth
|
||||
};
|
||||
canvas.DrawRoundRect(rect, borderPaint);
|
||||
}
|
||||
|
||||
// Calculate content bounds
|
||||
var contentBounds = new SKRect(
|
||||
bounds.Left + (float)Padding.Left,
|
||||
bounds.Top + (float)Padding.Top,
|
||||
bounds.Right - (float)Padding.Right,
|
||||
bounds.Bottom - (float)Padding.Bottom);
|
||||
|
||||
// Reserve space for clear button if shown
|
||||
var clearButtonSize = 20f;
|
||||
var clearButtonMargin = 8f;
|
||||
var showClear = ShouldShowClearButton();
|
||||
if (showClear)
|
||||
{
|
||||
contentBounds.Right -= clearButtonSize + clearButtonMargin;
|
||||
}
|
||||
|
||||
// Set up clipping for text area
|
||||
canvas.Save();
|
||||
canvas.ClipRect(contentBounds);
|
||||
|
||||
var fontStyle = GetFontStyle();
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
|
||||
using var font = new SKFont(typeface, (float)FontSize);
|
||||
using var paint = new SKPaint(font) { IsAntialias = true };
|
||||
|
||||
var displayText = GetDisplayText();
|
||||
// Append pre-edit text at cursor position for IME composition display
|
||||
var preEditInsertPos = Math.Min(_cursorPosition, displayText.Length);
|
||||
var displayTextWithPreEdit = string.IsNullOrEmpty(_preEditText)
|
||||
? displayText
|
||||
: displayText.Insert(preEditInsertPos, _preEditText);
|
||||
var hasText = !string.IsNullOrEmpty(displayTextWithPreEdit);
|
||||
|
||||
if (hasText)
|
||||
{
|
||||
paint.Color = GetEffectiveTextColor();
|
||||
|
||||
// Measure text to cursor position for scrolling
|
||||
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
|
||||
var cursorX = paint.MeasureText(textToCursor);
|
||||
|
||||
// Auto-scroll to keep cursor visible
|
||||
if (cursorX - _scrollOffset > contentBounds.Width - 10)
|
||||
{
|
||||
_scrollOffset = cursorX - contentBounds.Width + 10;
|
||||
}
|
||||
else if (cursorX - _scrollOffset < 0)
|
||||
{
|
||||
_scrollOffset = cursorX;
|
||||
}
|
||||
|
||||
// Draw selection (check != 0 to handle both forward and backward selection)
|
||||
if (IsFocused && _selectionLength != 0)
|
||||
{
|
||||
DrawSelection(canvas, paint, displayText, contentBounds);
|
||||
}
|
||||
|
||||
// Calculate text position based on vertical alignment
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(displayText, ref textBounds);
|
||||
|
||||
float x = contentBounds.Left - _scrollOffset;
|
||||
float y = VerticalTextAlignment switch
|
||||
{
|
||||
TextAlignment.Start => contentBounds.Top - textBounds.Top,
|
||||
TextAlignment.End => contentBounds.Bottom - textBounds.Bottom,
|
||||
_ => contentBounds.MidY - textBounds.MidY // Center
|
||||
};
|
||||
|
||||
// Draw the text with font fallback for emoji/CJK support
|
||||
DrawTextWithFallback(canvas, displayTextWithPreEdit, x, y, paint, typeface);
|
||||
|
||||
// Draw underline for pre-edit (composition) text
|
||||
if (!string.IsNullOrEmpty(_preEditText))
|
||||
{
|
||||
DrawPreEditUnderline(canvas, paint, displayText, x, y, contentBounds);
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if (IsFocused && !IsReadOnly && _cursorVisible)
|
||||
{
|
||||
DrawCursor(canvas, paint, displayText, contentBounds);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Placeholder))
|
||||
{
|
||||
// Draw placeholder
|
||||
paint.Color = GetEffectivePlaceholderColor();
|
||||
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(Placeholder, ref textBounds);
|
||||
|
||||
float x = contentBounds.Left;
|
||||
float y = contentBounds.MidY - textBounds.MidY;
|
||||
|
||||
canvas.DrawText(Placeholder, x, y, paint);
|
||||
}
|
||||
else if (IsFocused && !IsReadOnly && _cursorVisible)
|
||||
{
|
||||
// Draw cursor even with no text
|
||||
DrawCursor(canvas, paint, "", contentBounds);
|
||||
}
|
||||
|
||||
canvas.Restore();
|
||||
|
||||
// Draw clear button if applicable
|
||||
if (showClear)
|
||||
{
|
||||
DrawClearButton(canvas, bounds, clearButtonSize, clearButtonMargin);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldShowClearButton()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text)) return false;
|
||||
|
||||
// Check both legacy ShowClearButton and MAUI ClearButtonVisibility
|
||||
if (ShowClearButton && IsFocused) return true;
|
||||
|
||||
return ClearButtonVisibility switch
|
||||
{
|
||||
ClearButtonVisibility.WhileEditing => IsFocused,
|
||||
ClearButtonVisibility.Never => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawClearButton(SKCanvas canvas, SKRect bounds, float size, float margin)
|
||||
{
|
||||
var centerX = bounds.Right - margin - size / 2;
|
||||
var centerY = bounds.MidY;
|
||||
|
||||
// Draw circle background
|
||||
using var circlePaint = new SKPaint
|
||||
{
|
||||
Color = SkiaTheme.Gray400SK,
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawCircle(centerX, centerY, size / 2 - 2, circlePaint);
|
||||
|
||||
// Draw X
|
||||
using var xPaint = new SKPaint
|
||||
{
|
||||
Color = SkiaTheme.BackgroundWhiteSK,
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 2,
|
||||
StrokeCap = SKStrokeCap.Round
|
||||
};
|
||||
|
||||
var offset = size / 4 - 1;
|
||||
canvas.DrawLine(centerX - offset, centerY - offset, centerX + offset, centerY + offset, xPaint);
|
||||
canvas.DrawLine(centerX - offset, centerY + offset, centerX + offset, centerY - offset, xPaint);
|
||||
}
|
||||
|
||||
private void DrawSelection(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds)
|
||||
{
|
||||
var selStart = Math.Min(_selectionStart, _selectionStart + _selectionLength);
|
||||
var selEnd = Math.Max(_selectionStart, _selectionStart + _selectionLength);
|
||||
|
||||
var textToStart = displayText.Substring(0, selStart);
|
||||
var textToEnd = displayText.Substring(0, selEnd);
|
||||
|
||||
var startX = bounds.Left - _scrollOffset + paint.MeasureText(textToStart);
|
||||
var endX = bounds.Left - _scrollOffset + paint.MeasureText(textToEnd);
|
||||
|
||||
using var selPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(SelectionColor),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
canvas.DrawRect(startX, bounds.Top, endX - startX, bounds.Height, selPaint);
|
||||
}
|
||||
|
||||
private void DrawCursor(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds)
|
||||
{
|
||||
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
|
||||
var cursorX = bounds.Left - _scrollOffset + paint.MeasureText(textToCursor);
|
||||
|
||||
using var cursorPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(CursorColor),
|
||||
StrokeWidth = 2,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
canvas.DrawLine(cursorX, bounds.Top + 2, cursorX, bounds.Bottom - 2, cursorPaint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws text with font fallback for emoji, CJK, and other scripts.
|
||||
/// </summary>
|
||||
private void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface)
|
||||
=> TextRenderingHelper.DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface, (float)FontSize);
|
||||
|
||||
/// <summary>
|
||||
/// Draws underline for IME pre-edit (composition) text.
|
||||
/// </summary>
|
||||
private void DrawPreEditUnderline(SKCanvas canvas, SKPaint paint, string displayText, float x, float y, SKRect bounds)
|
||||
=> TextRenderingHelper.DrawPreEditUnderline(canvas, paint, displayText, _cursorPosition, _preEditText, x, y);
|
||||
|
||||
private void ResetCursorBlink()
|
||||
{
|
||||
_cursorBlinkTime = DateTime.UtcNow;
|
||||
_cursorVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates cursor blink animation.
|
||||
/// </summary>
|
||||
public void UpdateCursorBlink()
|
||||
{
|
||||
if (!IsFocused) return;
|
||||
|
||||
var elapsed = (DateTime.UtcNow - _cursorBlinkTime).TotalMilliseconds;
|
||||
var newVisible = ((int)(elapsed / 500) % 2) == 0;
|
||||
|
||||
if (newVisible != _cursorVisible)
|
||||
{
|
||||
_cursorVisible = newVisible;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var fontStyle = GetFontStyle();
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
|
||||
using var font = new SKFont(typeface, (float)FontSize);
|
||||
|
||||
// Use font metrics for consistent height regardless of text content
|
||||
// This prevents size changes when placeholder disappears or text changes
|
||||
var metrics = font.Metrics;
|
||||
var textHeight = metrics.Descent - metrics.Ascent + metrics.Leading;
|
||||
|
||||
return new Size(
|
||||
200, // Default width, will be overridden by layout
|
||||
textHeight + Padding.Top + Padding.Bottom + BorderWidth * 2);
|
||||
}
|
||||
}
|
||||
654
Views/SkiaEntry.Input.cs
Normal file
654
Views/SkiaEntry.Input.cs
Normal file
@@ -0,0 +1,654 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public partial class SkiaEntry
|
||||
{
|
||||
#region IInputContext Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text for IME context.
|
||||
/// </summary>
|
||||
string IInputContext.Text
|
||||
{
|
||||
get => Text;
|
||||
set => Text = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor position for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.CursorPosition
|
||||
{
|
||||
get => _cursorPosition;
|
||||
set => CursorPosition = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selection start for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.SelectionStart => _selectionStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selection length for IME context.
|
||||
/// </summary>
|
||||
int IInputContext.SelectionLength => _selectionLength;
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME commits text.
|
||||
/// </summary>
|
||||
public void OnTextCommitted(string text)
|
||||
{
|
||||
if (IsReadOnly) return;
|
||||
|
||||
// Delete selection if any
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
|
||||
// Clear pre-edit text
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
|
||||
// Check max length
|
||||
if (MaxLength > 0 && Text.Length + text.Length > MaxLength)
|
||||
{
|
||||
text = text.Substring(0, MaxLength - Text.Length);
|
||||
}
|
||||
|
||||
// Insert committed text at cursor
|
||||
var newText = Text.Insert(_cursorPosition, text);
|
||||
var newPos = _cursorPosition + text.Length;
|
||||
Text = newText;
|
||||
_cursorPosition = newPos;
|
||||
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME pre-edit (composition) text changes.
|
||||
/// </summary>
|
||||
public void OnPreEditChanged(string preEditText, int cursorPosition)
|
||||
{
|
||||
_preEditText = preEditText ?? string.Empty;
|
||||
_preEditCursorPosition = cursorPosition;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when IME pre-edit ends (cancelled or committed).
|
||||
/// </summary>
|
||||
public void OnPreEditEnded()
|
||||
{
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void OnTextInput(TextInputEventArgs e)
|
||||
{
|
||||
if (!IsEnabled || IsReadOnly) return;
|
||||
|
||||
// Ignore control characters (Ctrl+key combinations send ASCII control codes)
|
||||
if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32)
|
||||
return;
|
||||
|
||||
// Delete selection if any
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
|
||||
// Check max length
|
||||
if (MaxLength > 0 && Text.Length >= MaxLength)
|
||||
return;
|
||||
|
||||
// Insert text at cursor
|
||||
var insertText = e.Text;
|
||||
if (MaxLength > 0)
|
||||
{
|
||||
var remaining = MaxLength - Text.Length;
|
||||
insertText = insertText.Substring(0, Math.Min(insertText.Length, remaining));
|
||||
}
|
||||
|
||||
var newText = Text.Insert(_cursorPosition, insertText);
|
||||
var oldPos = _cursorPosition;
|
||||
Text = newText;
|
||||
_cursorPosition = oldPos + insertText.Length;
|
||||
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Backspace:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (_selectionLength > 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
else if (_cursorPosition > 0)
|
||||
{
|
||||
var newText = Text.Remove(_cursorPosition - 1, 1);
|
||||
var newPos = _cursorPosition - 1;
|
||||
Text = newText;
|
||||
_cursorPosition = newPos;
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Delete:
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (_selectionLength > 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
else if (_cursorPosition < Text.Length)
|
||||
{
|
||||
Text = Text.Remove(_cursorPosition, 1);
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Left:
|
||||
if (_cursorPosition > 0)
|
||||
{
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
ExtendSelection(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelection();
|
||||
_cursorPosition--;
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
if (_cursorPosition < Text.Length)
|
||||
{
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
ExtendSelection(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelection();
|
||||
_cursorPosition++;
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Home:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
ExtendSelectionTo(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelection();
|
||||
_cursorPosition = 0;
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.End:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
ExtendSelectionTo(Text.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelection();
|
||||
_cursorPosition = Text.Length;
|
||||
}
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.A:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.C:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
CopyToClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.V:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly)
|
||||
{
|
||||
PasteFromClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.X:
|
||||
if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly)
|
||||
{
|
||||
CutToClipboard();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
// Execute ReturnCommand if set and can execute
|
||||
if (ReturnCommand?.CanExecute(ReturnCommandParameter) == true)
|
||||
{
|
||||
ReturnCommand.Execute(ReturnCommandParameter);
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
// Handle right-click context menu
|
||||
if (e.Button == PointerButton.Right)
|
||||
{
|
||||
ShowContextMenu(e.X, e.Y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked on clear button
|
||||
if (ShouldShowClearButton())
|
||||
{
|
||||
var clearButtonSize = 20f;
|
||||
var clearButtonMargin = 8f;
|
||||
var clearCenterX = (float)(Bounds.Left + Bounds.Width) - clearButtonMargin - clearButtonSize / 2;
|
||||
var clearCenterY = (float)(Bounds.Top + Bounds.Height / 2);
|
||||
|
||||
var dx = e.X - clearCenterX;
|
||||
var dy = e.Y - clearCenterY;
|
||||
if (dx * dx + dy * dy < (clearButtonSize / 2) * (clearButtonSize / 2))
|
||||
{
|
||||
// Clear button clicked
|
||||
Text = "";
|
||||
_cursorPosition = 0;
|
||||
_selectionLength = 0;
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate cursor position from click using screen coordinates
|
||||
var screenBounds = ScreenBounds;
|
||||
var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset;
|
||||
_cursorPosition = GetCharacterIndexAtX(clickX);
|
||||
|
||||
// Check for double-click (select word or select all)
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||
var distanceFromLastClick = Math.Abs(e.X - _lastClickX);
|
||||
|
||||
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
|
||||
{
|
||||
// Double-click: select all or select word based on property
|
||||
if (SelectAllOnDoubleClick)
|
||||
{
|
||||
SelectAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectWordAtCursor();
|
||||
}
|
||||
_lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues
|
||||
_isSelecting = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single click: start selection
|
||||
_selectionStart = _cursorPosition;
|
||||
_selectionLength = 0;
|
||||
_isSelecting = true;
|
||||
_lastClickTime = now;
|
||||
_lastClickX = e.X;
|
||||
}
|
||||
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void SelectWordAtCursor()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text)) return;
|
||||
|
||||
// Find word boundaries
|
||||
int start = _cursorPosition;
|
||||
int end = _cursorPosition;
|
||||
|
||||
// Move start backwards to beginning of word
|
||||
while (start > 0 && IsWordChar(Text[start - 1]))
|
||||
start--;
|
||||
|
||||
// Move end forwards to end of word
|
||||
while (end < Text.Length && IsWordChar(Text[end]))
|
||||
end++;
|
||||
|
||||
_selectionStart = start;
|
||||
_cursorPosition = end;
|
||||
_selectionLength = end - start;
|
||||
}
|
||||
|
||||
private static bool IsWordChar(char c)
|
||||
{
|
||||
return char.IsLetterOrDigit(c) || c == '_';
|
||||
}
|
||||
|
||||
public override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
if (!IsEnabled || !_isSelecting) return;
|
||||
|
||||
// Extend selection to current mouse position
|
||||
var screenBounds = ScreenBounds;
|
||||
var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset;
|
||||
var newPosition = GetCharacterIndexAtX(clickX);
|
||||
|
||||
if (newPosition != _cursorPosition)
|
||||
{
|
||||
_cursorPosition = newPosition;
|
||||
_selectionLength = _cursorPosition - _selectionStart;
|
||||
ResetCursorBlink();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerReleased(PointerEventArgs e)
|
||||
{
|
||||
_isSelecting = false;
|
||||
}
|
||||
|
||||
private int GetCharacterIndexAtX(float x)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text)) return 0;
|
||||
|
||||
var fontStyle = GetFontStyle();
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
|
||||
using var font = new SKFont(typeface, (float)FontSize);
|
||||
using var paint = new SKPaint(font);
|
||||
|
||||
var displayText = GetDisplayText();
|
||||
|
||||
for (int i = 0; i <= displayText.Length; i++)
|
||||
{
|
||||
var substring = displayText.Substring(0, i);
|
||||
var width = paint.MeasureText(substring);
|
||||
|
||||
if (width >= x)
|
||||
{
|
||||
// Check if closer to current or previous character
|
||||
if (i > 0)
|
||||
{
|
||||
var prevWidth = paint.MeasureText(displayText.Substring(0, i - 1));
|
||||
if (x - prevWidth < width - x)
|
||||
return i - 1;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return displayText.Length;
|
||||
}
|
||||
|
||||
private void DeleteSelection()
|
||||
{
|
||||
var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
|
||||
var length = Math.Abs(_selectionLength);
|
||||
|
||||
Text = Text.Remove(start, length);
|
||||
_cursorPosition = start;
|
||||
_selectionLength = 0;
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
_selectionLength = 0;
|
||||
}
|
||||
|
||||
private void ExtendSelection(int delta)
|
||||
{
|
||||
if (_selectionLength == 0)
|
||||
{
|
||||
_selectionStart = _cursorPosition;
|
||||
}
|
||||
|
||||
_cursorPosition += delta;
|
||||
_selectionLength = _cursorPosition - _selectionStart;
|
||||
}
|
||||
|
||||
private void ExtendSelectionTo(int position)
|
||||
{
|
||||
if (_selectionLength == 0)
|
||||
{
|
||||
_selectionStart = _cursorPosition;
|
||||
}
|
||||
|
||||
_cursorPosition = position;
|
||||
_selectionLength = _cursorPosition - _selectionStart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects all text.
|
||||
/// </summary>
|
||||
public void SelectAll()
|
||||
{
|
||||
_selectionStart = 0;
|
||||
_cursorPosition = Text.Length;
|
||||
_selectionLength = Text.Length;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void CopyToClipboard()
|
||||
{
|
||||
// Password fields should not allow copying
|
||||
if (IsPassword) return;
|
||||
if (_selectionLength == 0) return;
|
||||
|
||||
var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
|
||||
var length = Math.Abs(_selectionLength);
|
||||
var selectedText = Text.Substring(start, length);
|
||||
|
||||
// Use system clipboard via xclip/xsel
|
||||
SystemClipboard.SetText(selectedText);
|
||||
}
|
||||
|
||||
private void CutToClipboard()
|
||||
{
|
||||
// Password fields should not allow cutting
|
||||
if (IsPassword) return;
|
||||
|
||||
CopyToClipboard();
|
||||
DeleteSelection();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void PasteFromClipboard()
|
||||
{
|
||||
// Get from system clipboard
|
||||
var text = SystemClipboard.GetText();
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
if (_selectionLength != 0)
|
||||
{
|
||||
DeleteSelection();
|
||||
}
|
||||
|
||||
// Check max length
|
||||
if (MaxLength > 0)
|
||||
{
|
||||
var remaining = MaxLength - Text.Length;
|
||||
text = text.Substring(0, Math.Min(text.Length, remaining));
|
||||
}
|
||||
|
||||
var newText = Text.Insert(_cursorPosition, text);
|
||||
var newPos = _cursorPosition + text.Length;
|
||||
Text = newText;
|
||||
_cursorPosition = newPos;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void ShowContextMenu(float x, float y)
|
||||
{
|
||||
DiagnosticLog.Debug("SkiaEntry", $"ShowContextMenu at ({x}, {y}), IsGtkMode={LinuxApplication.IsGtkMode}");
|
||||
bool hasSelection = _selectionLength != 0;
|
||||
bool hasText = !string.IsNullOrEmpty(Text);
|
||||
bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText());
|
||||
|
||||
if (LinuxApplication.IsGtkMode)
|
||||
{
|
||||
// Use GTK context menu when running in GTK mode (e.g., with WebView)
|
||||
GtkContextMenuService.ShowContextMenu(new List<GtkMenuItem>
|
||||
{
|
||||
new GtkMenuItem("Cut", () =>
|
||||
{
|
||||
CutToClipboard();
|
||||
Invalidate();
|
||||
}, hasSelection),
|
||||
new GtkMenuItem("Copy", () =>
|
||||
{
|
||||
CopyToClipboard();
|
||||
}, hasSelection),
|
||||
new GtkMenuItem("Paste", () =>
|
||||
{
|
||||
PasteFromClipboard();
|
||||
Invalidate();
|
||||
}, hasClipboard),
|
||||
GtkMenuItem.Separator,
|
||||
new GtkMenuItem("Select All", () =>
|
||||
{
|
||||
SelectAll();
|
||||
Invalidate();
|
||||
}, hasText)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use Skia-rendered context menu for pure Skia mode (Wayland/X11)
|
||||
bool isDarkTheme = Application.Current?.RequestedTheme == AppTheme.Dark;
|
||||
var items = new List<ContextMenuItem>
|
||||
{
|
||||
new ContextMenuItem("Cut", () =>
|
||||
{
|
||||
CutToClipboard();
|
||||
Invalidate();
|
||||
}, hasSelection),
|
||||
new ContextMenuItem("Copy", () =>
|
||||
{
|
||||
CopyToClipboard();
|
||||
}, hasSelection),
|
||||
new ContextMenuItem("Paste", () =>
|
||||
{
|
||||
PasteFromClipboard();
|
||||
Invalidate();
|
||||
}, hasClipboard),
|
||||
ContextMenuItem.Separator,
|
||||
new ContextMenuItem("Select All", () =>
|
||||
{
|
||||
SelectAll();
|
||||
Invalidate();
|
||||
}, hasText)
|
||||
};
|
||||
var menu = new SkiaContextMenu(x, y, items, isDarkTheme);
|
||||
LinuxDialogService.ShowContextMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnFocusGained()
|
||||
{
|
||||
base.OnFocusGained();
|
||||
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused);
|
||||
|
||||
// Connect to IME service
|
||||
_inputMethodService?.SetFocus(this);
|
||||
|
||||
// Update cursor location for IME candidate window positioning
|
||||
UpdateImeCursorLocation();
|
||||
}
|
||||
|
||||
public override void OnFocusLost()
|
||||
{
|
||||
base.OnFocusLost();
|
||||
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal);
|
||||
|
||||
// Disconnect from IME service and reset any composition
|
||||
_inputMethodService?.SetFocus(null);
|
||||
_preEditText = string.Empty;
|
||||
_preEditCursorPosition = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the IME cursor location for candidate window positioning.
|
||||
/// </summary>
|
||||
private void UpdateImeCursorLocation()
|
||||
{
|
||||
if (_inputMethodService == null) return;
|
||||
|
||||
var screenBounds = ScreenBounds;
|
||||
var fontStyle = GetFontStyle();
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
|
||||
using var font = new SKFont(typeface, (float)FontSize);
|
||||
using var paint = new SKPaint(font);
|
||||
|
||||
var displayText = GetDisplayText();
|
||||
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
|
||||
var cursorX = paint.MeasureText(textToCursor);
|
||||
|
||||
int x = (int)(screenBounds.Left + Padding.Left - _scrollOffset + cursorX);
|
||||
int y = (int)(screenBounds.Top + Padding.Top);
|
||||
int height = (int)FontSize;
|
||||
|
||||
_inputMethodService.SetCursorLocation(x, y, 2, height);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
500
Views/SkiaGrid.cs
Normal file
500
Views/SkiaGrid.cs
Normal file
@@ -0,0 +1,500 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Grid layout that arranges children in rows and columns.
|
||||
/// </summary>
|
||||
public class SkiaGrid : SkiaLayoutView
|
||||
{
|
||||
#region BindableProperties
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for RowSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty RowSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(RowSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaGrid),
|
||||
0f,
|
||||
BindingMode.TwoWay,
|
||||
propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ColumnSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ColumnSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ColumnSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaGrid),
|
||||
0f,
|
||||
BindingMode.TwoWay,
|
||||
propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure());
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly List<GridLength> _rowDefinitions = new();
|
||||
private readonly List<GridLength> _columnDefinitions = new();
|
||||
private readonly Dictionary<SkiaView, GridPosition> _childPositions = new();
|
||||
|
||||
private float[] _rowHeights = Array.Empty<float>();
|
||||
private float[] _columnWidths = Array.Empty<float>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row definitions.
|
||||
/// </summary>
|
||||
public IList<GridLength> RowDefinitions => _rowDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column definitions.
|
||||
/// </summary>
|
||||
public IList<GridLength> ColumnDefinitions => _columnDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Spacing between rows.
|
||||
/// </summary>
|
||||
public float RowSpacing
|
||||
{
|
||||
get => (float)GetValue(RowSpacingProperty);
|
||||
set => SetValue(RowSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spacing between columns.
|
||||
/// </summary>
|
||||
public float ColumnSpacing
|
||||
{
|
||||
get => (float)GetValue(ColumnSpacingProperty);
|
||||
set => SetValue(ColumnSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child at the specified grid position.
|
||||
/// </summary>
|
||||
public void AddChild(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
base.AddChild(child);
|
||||
_childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan);
|
||||
}
|
||||
|
||||
public override void RemoveChild(SkiaView child)
|
||||
{
|
||||
base.RemoveChild(child);
|
||||
_childPositions.Remove(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grid position of a child.
|
||||
/// </summary>
|
||||
public GridPosition GetPosition(SkiaView child)
|
||||
{
|
||||
return _childPositions.TryGetValue(child, out var pos) ? pos : new GridPosition(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the grid position of a child.
|
||||
/// </summary>
|
||||
public void SetPosition(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
_childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan);
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var contentWidth = (float)(availableSize.Width - Padding.Left - Padding.Right);
|
||||
var contentHeight = (float)(availableSize.Height - Padding.Top - Padding.Bottom);
|
||||
|
||||
// Handle NaN/Infinity
|
||||
if (float.IsNaN(contentWidth) || float.IsInfinity(contentWidth)) contentWidth = 800;
|
||||
if (float.IsNaN(contentHeight) || float.IsInfinity(contentHeight)) contentHeight = float.PositiveInfinity;
|
||||
|
||||
var rowCount = Math.Max(1, _rowDefinitions.Count > 0 ? _rowDefinitions.Count : GetMaxRow() + 1);
|
||||
var columnCount = Math.Max(1, _columnDefinitions.Count > 0 ? _columnDefinitions.Count : GetMaxColumn() + 1);
|
||||
|
||||
// First pass: measure children in Auto columns to get natural widths
|
||||
var columnNaturalWidths = new float[columnCount];
|
||||
var rowNaturalHeights = new float[rowCount];
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
|
||||
// For Auto columns, measure with infinite width to get natural size
|
||||
var def = pos.Column < _columnDefinitions.Count ? _columnDefinitions[pos.Column] : GridLength.Star;
|
||||
if (def.IsAuto && pos.ColumnSpan == 1)
|
||||
{
|
||||
var childSize = child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var childWidth = double.IsNaN(childSize.Width) ? 0f : (float)childSize.Width;
|
||||
columnNaturalWidths[pos.Column] = Math.Max(columnNaturalWidths[pos.Column], childWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate column widths - handle Auto, Absolute, and Star
|
||||
_columnWidths = CalculateSizesWithAuto(_columnDefinitions, contentWidth, ColumnSpacing, columnCount, columnNaturalWidths);
|
||||
|
||||
// Second pass: measure all children with calculated column widths
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
|
||||
// Give infinite height for initial measure
|
||||
var childSize = child.Measure(new Size(cellWidth, double.PositiveInfinity));
|
||||
|
||||
// Track max height for each row
|
||||
// Cap infinite/very large heights - child returning infinity means it doesn't have a natural height
|
||||
var childHeight = (float)childSize.Height;
|
||||
if (float.IsNaN(childHeight) || float.IsInfinity(childHeight) || childHeight > 100000)
|
||||
{
|
||||
// Use a default minimum - will be expanded by Star sizing if finite height is available
|
||||
childHeight = 44; // Standard row height
|
||||
}
|
||||
if (pos.RowSpan == 1)
|
||||
{
|
||||
rowNaturalHeights[pos.Row] = Math.Max(rowNaturalHeights[pos.Row], childHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate row heights - use natural heights when available height is infinite or very large
|
||||
// (Some layouts pass float.MaxValue instead of PositiveInfinity)
|
||||
if (float.IsInfinity(contentHeight) || contentHeight > 100000)
|
||||
{
|
||||
_rowHeights = rowNaturalHeights;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rowHeights = CalculateSizesWithAuto(_rowDefinitions, contentHeight, RowSpacing, rowCount, rowNaturalHeights);
|
||||
}
|
||||
|
||||
// Third pass: re-measure children with actual cell sizes
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
var cellHeight = GetCellHeight(pos.Row, pos.RowSpan);
|
||||
|
||||
child.Measure(new Size(cellWidth, cellHeight));
|
||||
}
|
||||
|
||||
// Calculate total size
|
||||
var totalWidth = _columnWidths.Sum() + Math.Max(0, columnCount - 1) * ColumnSpacing;
|
||||
var totalHeight = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing;
|
||||
|
||||
return new Size(
|
||||
totalWidth + Padding.Left + Padding.Right,
|
||||
totalHeight + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
|
||||
private int GetMaxRow()
|
||||
{
|
||||
int maxRow = 0;
|
||||
foreach (var pos in _childPositions.Values)
|
||||
{
|
||||
maxRow = Math.Max(maxRow, pos.Row + pos.RowSpan - 1);
|
||||
}
|
||||
return maxRow;
|
||||
}
|
||||
|
||||
private int GetMaxColumn()
|
||||
{
|
||||
int maxCol = 0;
|
||||
foreach (var pos in _childPositions.Values)
|
||||
{
|
||||
maxCol = Math.Max(maxCol, pos.Column + pos.ColumnSpan - 1);
|
||||
}
|
||||
return maxCol;
|
||||
}
|
||||
|
||||
private float[] CalculateSizesWithAuto(List<GridLength> definitions, float available, float spacing, int count, float[] naturalSizes)
|
||||
{
|
||||
if (count == 0) return new float[] { available };
|
||||
|
||||
var sizes = new float[count];
|
||||
var totalSpacing = Math.Max(0, count - 1) * spacing;
|
||||
var remainingSpace = available - totalSpacing;
|
||||
|
||||
// First pass: absolute and auto sizes
|
||||
float starTotal = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var def = i < definitions.Count ? definitions[i] : GridLength.Star;
|
||||
|
||||
if (def.IsAbsolute)
|
||||
{
|
||||
sizes[i] = def.Value;
|
||||
remainingSpace -= def.Value;
|
||||
}
|
||||
else if (def.IsAuto)
|
||||
{
|
||||
// Use natural size from measured children
|
||||
sizes[i] = naturalSizes[i];
|
||||
remainingSpace -= sizes[i];
|
||||
}
|
||||
else if (def.IsStar)
|
||||
{
|
||||
starTotal += def.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: star sizes (distribute remaining space)
|
||||
if (starTotal > 0 && remainingSpace > 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var def = i < definitions.Count ? definitions[i] : GridLength.Star;
|
||||
if (def.IsStar)
|
||||
{
|
||||
sizes[i] = (def.Value / starTotal) * remainingSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
private float GetCellWidth(int column, int span)
|
||||
{
|
||||
float width = 0;
|
||||
for (int i = column; i < Math.Min(column + span, _columnWidths.Length); i++)
|
||||
{
|
||||
width += _columnWidths[i];
|
||||
if (i > column) width += ColumnSpacing;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
private float GetCellHeight(int row, int span)
|
||||
{
|
||||
float height = 0;
|
||||
for (int i = row; i < Math.Min(row + span, _rowHeights.Length); i++)
|
||||
{
|
||||
height += _rowHeights[i];
|
||||
if (i > row) height += RowSpacing;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private float GetColumnOffset(int column)
|
||||
{
|
||||
float offset = 0;
|
||||
for (int i = 0; i < Math.Min(column, _columnWidths.Length); i++)
|
||||
{
|
||||
offset += _columnWidths[i] + ColumnSpacing;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
private float GetRowOffset(int row)
|
||||
{
|
||||
float offset = 0;
|
||||
for (int i = 0; i < Math.Min(row, _rowHeights.Length); i++)
|
||||
{
|
||||
offset += _rowHeights[i] + RowSpacing;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected override Rect ArrangeOverride(Rect bounds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
||||
|
||||
// Recalculate row heights for arrange bounds if they differ from measurement
|
||||
// This ensures Star rows expand to fill available space
|
||||
var rowCount = _rowHeights.Length > 0 ? _rowHeights.Length : 1;
|
||||
var columnCount = _columnWidths.Length > 0 ? _columnWidths.Length : 1;
|
||||
var arrangeRowHeights = _rowHeights;
|
||||
|
||||
// If we have arrange height and rows need recalculating
|
||||
if (content.Height > 0 && !float.IsInfinity(content.Height))
|
||||
{
|
||||
var measuredRowsTotal = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing;
|
||||
|
||||
// If arrange height is larger than measured, redistribute to Star rows
|
||||
if (content.Height > measuredRowsTotal + 1)
|
||||
{
|
||||
arrangeRowHeights = new float[rowCount];
|
||||
var extraHeight = content.Height - measuredRowsTotal;
|
||||
|
||||
// Count Star rows (implicit rows without definitions are Star)
|
||||
float totalStarWeight = 0;
|
||||
for (int i = 0; i < rowCount; i++)
|
||||
{
|
||||
var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star;
|
||||
if (def.IsStar) totalStarWeight += def.Value;
|
||||
}
|
||||
|
||||
// Distribute extra height to Star rows
|
||||
for (int i = 0; i < rowCount; i++)
|
||||
{
|
||||
var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star;
|
||||
arrangeRowHeights[i] = i < _rowHeights.Length ? _rowHeights[i] : 0;
|
||||
|
||||
if (def.IsStar && totalStarWeight > 0)
|
||||
{
|
||||
arrangeRowHeights[i] += extraHeight * (def.Value / totalStarWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
arrangeRowHeights = _rowHeights;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
|
||||
var x = content.Left + GetColumnOffset(pos.Column);
|
||||
|
||||
// Calculate y using arrange row heights
|
||||
float y = content.Top;
|
||||
for (int i = 0; i < Math.Min(pos.Row, arrangeRowHeights.Length); i++)
|
||||
{
|
||||
y += arrangeRowHeights[i] + RowSpacing;
|
||||
}
|
||||
|
||||
var width = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
|
||||
// Calculate height using arrange row heights
|
||||
float height = 0;
|
||||
for (int i = pos.Row; i < Math.Min(pos.Row + pos.RowSpan, arrangeRowHeights.Length); i++)
|
||||
{
|
||||
height += arrangeRowHeights[i];
|
||||
if (i > pos.Row) height += RowSpacing;
|
||||
}
|
||||
|
||||
// Clamp infinite dimensions
|
||||
if (float.IsInfinity(width) || float.IsNaN(width))
|
||||
width = content.Width;
|
||||
if (float.IsInfinity(height) || float.IsNaN(height) || height <= 0)
|
||||
height = content.Height;
|
||||
|
||||
// Apply child's margin
|
||||
var margin = child.Margin;
|
||||
var cellX = x + (float)margin.Left;
|
||||
var cellY = y + (float)margin.Top;
|
||||
var cellWidth = width - (float)margin.Left - (float)margin.Right;
|
||||
var cellHeight = height - (float)margin.Top - (float)margin.Bottom;
|
||||
|
||||
// Get child's desired size
|
||||
var childDesiredSize = child.Measure(new Size(cellWidth, cellHeight));
|
||||
var childWidth = (float)childDesiredSize.Width;
|
||||
var childHeight = (float)childDesiredSize.Height;
|
||||
|
||||
var vAlign = (int)child.VerticalOptions.Alignment;
|
||||
|
||||
// Apply HorizontalOptions
|
||||
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
||||
float finalX = cellX;
|
||||
float finalWidth = cellWidth;
|
||||
var hAlign = (int)child.HorizontalOptions.Alignment;
|
||||
if (hAlign != 3 && childWidth < cellWidth && childWidth > 0) // 3 = Fill
|
||||
{
|
||||
finalWidth = childWidth;
|
||||
if (hAlign == 1) // Center
|
||||
finalX = cellX + (cellWidth - childWidth) / 2;
|
||||
else if (hAlign == 2) // End
|
||||
finalX = cellX + cellWidth - childWidth;
|
||||
}
|
||||
|
||||
// Apply VerticalOptions
|
||||
float finalY = cellY;
|
||||
float finalHeight = cellHeight;
|
||||
// vAlign already calculated above for debug logging
|
||||
if (vAlign != 3 && childHeight < cellHeight && childHeight > 0) // 3 = Fill
|
||||
{
|
||||
finalHeight = childHeight;
|
||||
if (vAlign == 1) // Center
|
||||
finalY = cellY + (cellHeight - childHeight) / 2;
|
||||
else if (vAlign == 2) // End
|
||||
finalY = cellY + cellHeight - childHeight;
|
||||
}
|
||||
|
||||
child.Arrange(new Rect(finalX, finalY, finalWidth, finalHeight));
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DiagnosticLog.Error("SkiaGrid", $"EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
DiagnosticLog.Error("SkiaGrid", $"Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}");
|
||||
DiagnosticLog.Error("SkiaGrid", $"Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid position information.
|
||||
/// </summary>
|
||||
public readonly struct GridPosition
|
||||
{
|
||||
public int Row { get; }
|
||||
public int Column { get; }
|
||||
public int RowSpan { get; }
|
||||
public int ColumnSpan { get; }
|
||||
|
||||
public GridPosition(int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
Row = row;
|
||||
Column = column;
|
||||
RowSpan = Math.Max(1, rowSpan);
|
||||
ColumnSpan = Math.Max(1, columnSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid length specification.
|
||||
/// </summary>
|
||||
public readonly struct GridLength
|
||||
{
|
||||
public float Value { get; }
|
||||
public GridUnitType GridUnitType { get; }
|
||||
|
||||
public bool IsAbsolute => GridUnitType == GridUnitType.Absolute;
|
||||
public bool IsAuto => GridUnitType == GridUnitType.Auto;
|
||||
public bool IsStar => GridUnitType == GridUnitType.Star;
|
||||
|
||||
public static GridLength Auto => new(1, GridUnitType.Auto);
|
||||
public static GridLength Star => new(1, GridUnitType.Star);
|
||||
|
||||
public GridLength(float value, GridUnitType unitType = GridUnitType.Absolute)
|
||||
{
|
||||
Value = value;
|
||||
GridUnitType = unitType;
|
||||
}
|
||||
|
||||
public static GridLength FromAbsolute(float value) => new(value, GridUnitType.Absolute);
|
||||
public static GridLength FromStar(float value = 1) => new(value, GridUnitType.Star);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid unit type options.
|
||||
/// </summary>
|
||||
public enum GridUnitType
|
||||
{
|
||||
Absolute,
|
||||
Star,
|
||||
Auto
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Svg.Skia;
|
||||
|
||||
@@ -513,7 +514,7 @@ public class SkiaImage : SkiaView
|
||||
_isSvg = false;
|
||||
_currentFilePath = null;
|
||||
_cacheKey = null;
|
||||
Console.WriteLine($"[SkiaImage] File not found: {filePath}");
|
||||
DiagnosticLog.Warn("SkiaImage", $"File not found: {filePath}");
|
||||
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath)));
|
||||
return;
|
||||
}
|
||||
@@ -1210,7 +1211,7 @@ public class SkiaImage : SkiaView
|
||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream != null)
|
||||
{
|
||||
Console.WriteLine($"[SkiaImage] Loaded embedded resource: {resourceName}");
|
||||
DiagnosticLog.Debug("SkiaImage", $"Loaded embedded resource: {resourceName}");
|
||||
return (stream, requestedExt);
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1228,7 @@ public class SkiaImage : SkiaView
|
||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream != null)
|
||||
{
|
||||
Console.WriteLine($"[SkiaImage] Loaded SVG as PNG substitute: {resourceName}");
|
||||
DiagnosticLog.Debug("SkiaImage", $"Loaded SVG as PNG substitute: {resourceName}");
|
||||
return (stream, ".svg");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Svg.Skia;
|
||||
|
||||
@@ -422,7 +423,7 @@ public class SkiaImageButton : SkiaView
|
||||
{
|
||||
_isLoading = true;
|
||||
Invalidate();
|
||||
Console.WriteLine("[SkiaImageButton] LoadFromFileAsync: " + filePath);
|
||||
DiagnosticLog.Debug("SkiaImageButton", "LoadFromFileAsync: " + filePath);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -450,15 +451,15 @@ public class SkiaImageButton : SkiaView
|
||||
if (File.Exists(path))
|
||||
{
|
||||
foundPath = path;
|
||||
Console.WriteLine("[SkiaImageButton] Found file at: " + path);
|
||||
DiagnosticLog.Debug("SkiaImageButton", "Found file at: " + path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPath == null)
|
||||
{
|
||||
Console.WriteLine("[SkiaImageButton] File not found: " + filePath);
|
||||
Console.WriteLine("[SkiaImageButton] Searched paths: " + string.Join(", ", searchPaths));
|
||||
DiagnosticLog.Warn("SkiaImageButton", "File not found: " + filePath);
|
||||
DiagnosticLog.Debug("SkiaImageButton", "Searched paths: " + string.Join(", ", searchPaths));
|
||||
_isLoading = false;
|
||||
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath)));
|
||||
return;
|
||||
@@ -498,7 +499,7 @@ public class SkiaImageButton : SkiaView
|
||||
canvas.Translate(-cullRect.Left, -cullRect.Top);
|
||||
canvas.DrawPicture(svg.Picture);
|
||||
Bitmap = bitmap;
|
||||
Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({width}x{height}), cullRect={cullRect}");
|
||||
DiagnosticLog.Debug("SkiaImageButton", $"Loaded SVG: {foundPath} ({width}x{height}), cullRect={cullRect}");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -508,7 +509,7 @@ public class SkiaImageButton : SkiaView
|
||||
if (bitmap != null)
|
||||
{
|
||||
Bitmap = bitmap;
|
||||
Console.WriteLine("[SkiaImageButton] Loaded image: " + foundPath);
|
||||
DiagnosticLog.Debug("SkiaImageButton", "Loaded image: " + foundPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ using SkiaSharp;
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
@@ -114,7 +115,7 @@ public class SkiaItemsView : SkiaView
|
||||
|
||||
protected virtual void RefreshItems()
|
||||
{
|
||||
Console.WriteLine($"[SkiaItemsView] RefreshItems called, clearing {_items.Count} items and {_itemViewCache.Count} cached views");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"RefreshItems called, clearing {_items.Count} items and {_itemViewCache.Count} cached views");
|
||||
_items.Clear();
|
||||
_itemViewCache.Clear(); // Clear cached views when items change
|
||||
_itemHeights.Clear(); // Clear cached heights
|
||||
@@ -125,7 +126,7 @@ public class SkiaItemsView : SkiaView
|
||||
_items.Add(item);
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"[SkiaItemsView] RefreshItems done, now have {_items.Count} items");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"RefreshItems done, now have {_items.Count} items");
|
||||
_scrollOffset = 0;
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ public class SkiaItemsView : SkiaView
|
||||
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
Console.WriteLine($"[SkiaItemsView] OnDraw - bounds={bounds}, items={_items.Count}, ItemViewCreator={(ItemViewCreator != null ? "set" : "null")}");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"OnDraw - bounds={bounds}, items={_items.Count}, ItemViewCreator={(ItemViewCreator != null ? "set" : "null")}");
|
||||
|
||||
// Draw background
|
||||
if (BackgroundColor != null && BackgroundColor != Colors.Transparent)
|
||||
@@ -283,7 +284,7 @@ public class SkiaItemsView : SkiaView
|
||||
// Try to use ItemViewCreator for templated rendering
|
||||
if (ItemViewCreator != null)
|
||||
{
|
||||
Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator exists, item: {item}");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"DrawItem {index} - ItemViewCreator exists, item: {item}");
|
||||
// Get or create cached view for this index
|
||||
if (!_itemViewCache.TryGetValue(index, out var itemView) || itemView == null)
|
||||
{
|
||||
@@ -322,7 +323,7 @@ public class SkiaItemsView : SkiaView
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator is NULL, falling back to ToString");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"DrawItem {index} - ItemViewCreator is NULL, falling back to ToString");
|
||||
}
|
||||
|
||||
// Draw separator
|
||||
@@ -424,7 +425,7 @@ public class SkiaItemsView : SkiaView
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaItemsView] OnPointerPressed - x={e.X}, y={e.Y}, Bounds={Bounds}, ScreenBounds={ScreenBounds}, ItemCount={_items.Count}");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"OnPointerPressed - x={e.X}, y={e.Y}, Bounds={Bounds}, ScreenBounds={ScreenBounds}, ItemCount={_items.Count}");
|
||||
if (!IsEnabled) return;
|
||||
|
||||
// Check if clicking on scrollbar thumb
|
||||
@@ -537,7 +538,7 @@ public class SkiaItemsView : SkiaView
|
||||
cumulativeY += itemH + _itemSpacing;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[SkiaItemsView] Tap at Y={e.Y}, screenBounds.Top={screenBounds.Top}, scrollOffset={_scrollOffset}, localY={localY}, index={tappedIndex}");
|
||||
DiagnosticLog.Debug("SkiaItemsView", $"Tap at Y={e.Y}, screenBounds.Top={screenBounds.Top}, scrollOffset={_scrollOffset}, localY={localY}, index={tappedIndex}");
|
||||
|
||||
if (tappedIndex >= 0 && tappedIndex < _items.Count)
|
||||
{
|
||||
|
||||
@@ -606,11 +606,7 @@ public class SkiaLabel : SkiaView
|
||||
OnTextChanged();
|
||||
}
|
||||
|
||||
private SKColor ToSKColor(Color? color)
|
||||
{
|
||||
if (color == null) return SkiaTheme.TextPrimarySK;
|
||||
return color.ToSKColor();
|
||||
}
|
||||
private SKColor ToSKColor(Color? color) => TextRenderingHelper.ToSKColor(color, SkiaTheme.TextPrimarySK);
|
||||
|
||||
private string GetDisplayText()
|
||||
{
|
||||
@@ -631,16 +627,7 @@ public class SkiaLabel : SkiaView
|
||||
};
|
||||
}
|
||||
|
||||
private SKFontStyle GetFontStyle()
|
||||
{
|
||||
bool isBold = FontAttributes.HasFlag(FontAttributes.Bold);
|
||||
bool isItalic = FontAttributes.HasFlag(FontAttributes.Italic);
|
||||
|
||||
return new SKFontStyle(
|
||||
isBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
||||
SKFontStyleWidth.Normal,
|
||||
isItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
}
|
||||
private SKFontStyle GetFontStyle() => TextRenderingHelper.GetFontStyle(FontAttributes);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if text should be rendered right-to-left based on FlowDirection.
|
||||
@@ -878,39 +865,7 @@ public class SkiaLabel : SkiaView
|
||||
/// Draws text with font fallback for emoji, CJK, and other scripts.
|
||||
/// </summary>
|
||||
private void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use FontFallbackManager for mixed-script text
|
||||
var runs = FontFallbackManager.Instance.ShapeTextWithFallback(text, preferredTypeface);
|
||||
|
||||
if (runs.Count <= 1)
|
||||
{
|
||||
// Single run or no fallback needed - draw directly
|
||||
canvas.DrawText(text, x, y, paint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple runs with different fonts
|
||||
float fontSize = FontSize > 0 ? (float)FontSize : 14f;
|
||||
float currentX = x;
|
||||
|
||||
foreach (var run in runs)
|
||||
{
|
||||
using var runFont = new SKFont(run.Typeface, fontSize);
|
||||
using var runPaint = new SKPaint(runFont)
|
||||
{
|
||||
Color = paint.Color,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
canvas.DrawText(run.Text, currentX, y, runPaint);
|
||||
currentX += runPaint.MeasureText(run.Text);
|
||||
}
|
||||
}
|
||||
=> TextRenderingHelper.DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface, FontSize > 0 ? (float)FontSize : 14f);
|
||||
|
||||
/// <summary>
|
||||
/// Draws formatted span text with font fallback for emoji, CJK, and other scripts.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui;
|
||||
|
||||
@@ -227,10 +228,10 @@ public abstract class SkiaLayoutView : SkiaView
|
||||
}
|
||||
if (hasCV)
|
||||
{
|
||||
Console.WriteLine($"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}");
|
||||
DiagnosticLog.Debug("SkiaLayoutView", $"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}");
|
||||
foreach (var c in _children)
|
||||
{
|
||||
Console.WriteLine($"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}");
|
||||
DiagnosticLog.Debug("SkiaLayoutView", $"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,860 +315,3 @@ public abstract class SkiaLayoutView : SkiaView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stack layout that arranges children in a horizontal or vertical line.
|
||||
/// </summary>
|
||||
public class SkiaStackLayout : SkiaLayoutView
|
||||
{
|
||||
/// <summary>
|
||||
/// Bindable property for Orientation.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty OrientationProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(Orientation),
|
||||
typeof(StackOrientation),
|
||||
typeof(SkiaStackLayout),
|
||||
StackOrientation.Vertical,
|
||||
BindingMode.TwoWay,
|
||||
propertyChanged: (b, o, n) => ((SkiaStackLayout)b).InvalidateMeasure());
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orientation of the stack.
|
||||
/// </summary>
|
||||
public StackOrientation Orientation
|
||||
{
|
||||
get => (StackOrientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
// Handle NaN/Infinity in padding
|
||||
var paddingLeft = (float)(double.IsNaN(Padding.Left) ? 0 : Padding.Left);
|
||||
var paddingRight = (float)(double.IsNaN(Padding.Right) ? 0 : Padding.Right);
|
||||
var paddingTop = (float)(double.IsNaN(Padding.Top) ? 0 : Padding.Top);
|
||||
var paddingBottom = (float)(double.IsNaN(Padding.Bottom) ? 0 : Padding.Bottom);
|
||||
|
||||
var contentWidth = (float)availableSize.Width - paddingLeft - paddingRight;
|
||||
var contentHeight = (float)availableSize.Height - paddingTop - paddingBottom;
|
||||
|
||||
// Clamp negative sizes to 0
|
||||
if (contentWidth < 0 || float.IsNaN(contentWidth)) contentWidth = 0;
|
||||
if (contentHeight < 0 || float.IsNaN(contentHeight)) contentHeight = 0;
|
||||
|
||||
float totalWidth = 0;
|
||||
float totalHeight = 0;
|
||||
float maxWidth = 0;
|
||||
float maxHeight = 0;
|
||||
|
||||
// For stack layouts, give children infinite size in the stacking direction
|
||||
// so they can measure to their natural size
|
||||
var childAvailable = Orientation == StackOrientation.Horizontal
|
||||
? new Size(double.PositiveInfinity, contentHeight) // Horizontal: infinite width, constrained height
|
||||
: new Size(contentWidth, double.PositiveInfinity); // Vertical: constrained width, infinite height
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var childSize = child.Measure(childAvailable);
|
||||
|
||||
// Skip NaN sizes from child measurements
|
||||
var childWidth = double.IsNaN(childSize.Width) ? 0f : (float)childSize.Width;
|
||||
var childHeight = double.IsNaN(childSize.Height) ? 0f : (float)childSize.Height;
|
||||
|
||||
if (Orientation == StackOrientation.Vertical)
|
||||
{
|
||||
totalHeight += childHeight;
|
||||
maxWidth = Math.Max(maxWidth, childWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
totalWidth += childWidth;
|
||||
maxHeight = Math.Max(maxHeight, childHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Add spacing
|
||||
var visibleCount = Children.Count(c => c.IsVisible);
|
||||
var totalSpacing = (float)(Math.Max(0, visibleCount - 1) * Spacing);
|
||||
|
||||
if (Orientation == StackOrientation.Vertical)
|
||||
{
|
||||
totalHeight += totalSpacing;
|
||||
return new Size(
|
||||
maxWidth + paddingLeft + paddingRight,
|
||||
totalHeight + paddingTop + paddingBottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
totalWidth += totalSpacing;
|
||||
return new Size(
|
||||
totalWidth + paddingLeft + paddingRight,
|
||||
maxHeight + paddingTop + paddingBottom);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Rect ArrangeOverride(Rect bounds)
|
||||
{
|
||||
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
||||
|
||||
// Clamp content dimensions if infinite - use reasonable defaults
|
||||
var contentWidth = float.IsInfinity(content.Width) || float.IsNaN(content.Width) ? 800f : content.Width;
|
||||
var contentHeight = float.IsInfinity(content.Height) || float.IsNaN(content.Height) ? 600f : content.Height;
|
||||
|
||||
float offset = 0;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var childDesired = child.DesiredSize;
|
||||
|
||||
// Handle NaN and Infinity in desired size
|
||||
var childWidth = double.IsNaN(childDesired.Width) || double.IsInfinity(childDesired.Width)
|
||||
? contentWidth
|
||||
: (float)childDesired.Width;
|
||||
var childHeight = double.IsNaN(childDesired.Height) || double.IsInfinity(childDesired.Height)
|
||||
? contentHeight
|
||||
: (float)childDesired.Height;
|
||||
|
||||
float childBoundsLeft, childBoundsTop, childBoundsWidth, childBoundsHeight;
|
||||
if (Orientation == StackOrientation.Vertical)
|
||||
{
|
||||
// For ScrollView children, give them the remaining viewport height
|
||||
// Clamp to avoid giving them their content size
|
||||
var remainingHeight = Math.Max(0, contentHeight - offset);
|
||||
var useHeight = child is SkiaScrollView
|
||||
? remainingHeight
|
||||
: Math.Min(childHeight, remainingHeight > 0 ? remainingHeight : childHeight);
|
||||
|
||||
// Respect child's HorizontalOptions for vertical layouts
|
||||
var useWidth = Math.Min(childWidth, contentWidth);
|
||||
float childLeft = content.Left;
|
||||
|
||||
var horizontalOptions = child.HorizontalOptions;
|
||||
var alignmentValue = (int)horizontalOptions.Alignment;
|
||||
|
||||
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
||||
if (alignmentValue == 1) // Center
|
||||
{
|
||||
childLeft = content.Left + (contentWidth - useWidth) / 2;
|
||||
}
|
||||
else if (alignmentValue == 2) // End
|
||||
{
|
||||
childLeft = content.Left + contentWidth - useWidth;
|
||||
}
|
||||
else if (alignmentValue == 3) // Fill
|
||||
{
|
||||
useWidth = contentWidth;
|
||||
}
|
||||
|
||||
childBoundsLeft = childLeft;
|
||||
childBoundsTop = content.Top + offset;
|
||||
childBoundsWidth = useWidth;
|
||||
childBoundsHeight = useHeight;
|
||||
offset += useHeight + (float)Spacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Horizontal stack: give each child its measured width
|
||||
// Don't constrain - let content overflow if needed (parent clips)
|
||||
var useWidth = childWidth;
|
||||
|
||||
// Respect child's VerticalOptions for horizontal layouts
|
||||
var useHeight = Math.Min(childHeight, contentHeight);
|
||||
float childTop = content.Top;
|
||||
float childBottomCalc = content.Top + useHeight;
|
||||
|
||||
var verticalOptions = child.VerticalOptions;
|
||||
var alignmentValue = (int)verticalOptions.Alignment;
|
||||
|
||||
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
||||
if (alignmentValue == 1) // Center
|
||||
{
|
||||
childTop = content.Top + (contentHeight - useHeight) / 2;
|
||||
childBottomCalc = childTop + useHeight;
|
||||
}
|
||||
else if (alignmentValue == 2) // End
|
||||
{
|
||||
childTop = content.Top + contentHeight - useHeight;
|
||||
childBottomCalc = content.Top + contentHeight;
|
||||
}
|
||||
else if (alignmentValue == 3) // Fill
|
||||
{
|
||||
childTop = content.Top;
|
||||
childBottomCalc = content.Top + contentHeight;
|
||||
}
|
||||
|
||||
childBoundsLeft = content.Left + offset;
|
||||
childBoundsTop = childTop;
|
||||
childBoundsWidth = useWidth;
|
||||
childBoundsHeight = childBottomCalc - childTop;
|
||||
offset += useWidth + (float)Spacing;
|
||||
}
|
||||
|
||||
// Apply child's margin
|
||||
var margin = child.Margin;
|
||||
var marginedBounds = new Rect(
|
||||
childBoundsLeft + (float)margin.Left,
|
||||
childBoundsTop + (float)margin.Top,
|
||||
childBoundsWidth - (float)margin.Left - (float)margin.Right,
|
||||
childBoundsHeight - (float)margin.Top - (float)margin.Bottom);
|
||||
child.Arrange(marginedBounds);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stack orientation options.
|
||||
/// </summary>
|
||||
public enum StackOrientation
|
||||
{
|
||||
Vertical,
|
||||
Horizontal
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid layout that arranges children in rows and columns.
|
||||
/// </summary>
|
||||
public class SkiaGrid : SkiaLayoutView
|
||||
{
|
||||
#region BindableProperties
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for RowSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty RowSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(RowSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaGrid),
|
||||
0f,
|
||||
BindingMode.TwoWay,
|
||||
propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ColumnSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ColumnSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ColumnSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaGrid),
|
||||
0f,
|
||||
BindingMode.TwoWay,
|
||||
propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure());
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly List<GridLength> _rowDefinitions = new();
|
||||
private readonly List<GridLength> _columnDefinitions = new();
|
||||
private readonly Dictionary<SkiaView, GridPosition> _childPositions = new();
|
||||
|
||||
private float[] _rowHeights = Array.Empty<float>();
|
||||
private float[] _columnWidths = Array.Empty<float>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row definitions.
|
||||
/// </summary>
|
||||
public IList<GridLength> RowDefinitions => _rowDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column definitions.
|
||||
/// </summary>
|
||||
public IList<GridLength> ColumnDefinitions => _columnDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Spacing between rows.
|
||||
/// </summary>
|
||||
public float RowSpacing
|
||||
{
|
||||
get => (float)GetValue(RowSpacingProperty);
|
||||
set => SetValue(RowSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spacing between columns.
|
||||
/// </summary>
|
||||
public float ColumnSpacing
|
||||
{
|
||||
get => (float)GetValue(ColumnSpacingProperty);
|
||||
set => SetValue(ColumnSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child at the specified grid position.
|
||||
/// </summary>
|
||||
public void AddChild(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
base.AddChild(child);
|
||||
_childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan);
|
||||
}
|
||||
|
||||
public override void RemoveChild(SkiaView child)
|
||||
{
|
||||
base.RemoveChild(child);
|
||||
_childPositions.Remove(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grid position of a child.
|
||||
/// </summary>
|
||||
public GridPosition GetPosition(SkiaView child)
|
||||
{
|
||||
return _childPositions.TryGetValue(child, out var pos) ? pos : new GridPosition(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the grid position of a child.
|
||||
/// </summary>
|
||||
public void SetPosition(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
_childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan);
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var contentWidth = (float)(availableSize.Width - Padding.Left - Padding.Right);
|
||||
var contentHeight = (float)(availableSize.Height - Padding.Top - Padding.Bottom);
|
||||
|
||||
// Handle NaN/Infinity
|
||||
if (float.IsNaN(contentWidth) || float.IsInfinity(contentWidth)) contentWidth = 800;
|
||||
if (float.IsNaN(contentHeight) || float.IsInfinity(contentHeight)) contentHeight = float.PositiveInfinity;
|
||||
|
||||
var rowCount = Math.Max(1, _rowDefinitions.Count > 0 ? _rowDefinitions.Count : GetMaxRow() + 1);
|
||||
var columnCount = Math.Max(1, _columnDefinitions.Count > 0 ? _columnDefinitions.Count : GetMaxColumn() + 1);
|
||||
|
||||
// First pass: measure children in Auto columns to get natural widths
|
||||
var columnNaturalWidths = new float[columnCount];
|
||||
var rowNaturalHeights = new float[rowCount];
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
|
||||
// For Auto columns, measure with infinite width to get natural size
|
||||
var def = pos.Column < _columnDefinitions.Count ? _columnDefinitions[pos.Column] : GridLength.Star;
|
||||
if (def.IsAuto && pos.ColumnSpan == 1)
|
||||
{
|
||||
var childSize = child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var childWidth = double.IsNaN(childSize.Width) ? 0f : (float)childSize.Width;
|
||||
columnNaturalWidths[pos.Column] = Math.Max(columnNaturalWidths[pos.Column], childWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate column widths - handle Auto, Absolute, and Star
|
||||
_columnWidths = CalculateSizesWithAuto(_columnDefinitions, contentWidth, ColumnSpacing, columnCount, columnNaturalWidths);
|
||||
|
||||
// Second pass: measure all children with calculated column widths
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
|
||||
// Give infinite height for initial measure
|
||||
var childSize = child.Measure(new Size(cellWidth, double.PositiveInfinity));
|
||||
|
||||
// Track max height for each row
|
||||
// Cap infinite/very large heights - child returning infinity means it doesn't have a natural height
|
||||
var childHeight = (float)childSize.Height;
|
||||
if (float.IsNaN(childHeight) || float.IsInfinity(childHeight) || childHeight > 100000)
|
||||
{
|
||||
// Use a default minimum - will be expanded by Star sizing if finite height is available
|
||||
childHeight = 44; // Standard row height
|
||||
}
|
||||
if (pos.RowSpan == 1)
|
||||
{
|
||||
rowNaturalHeights[pos.Row] = Math.Max(rowNaturalHeights[pos.Row], childHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate row heights - use natural heights when available height is infinite or very large
|
||||
// (Some layouts pass float.MaxValue instead of PositiveInfinity)
|
||||
if (float.IsInfinity(contentHeight) || contentHeight > 100000)
|
||||
{
|
||||
_rowHeights = rowNaturalHeights;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rowHeights = CalculateSizesWithAuto(_rowDefinitions, contentHeight, RowSpacing, rowCount, rowNaturalHeights);
|
||||
}
|
||||
|
||||
// Third pass: re-measure children with actual cell sizes
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
var cellHeight = GetCellHeight(pos.Row, pos.RowSpan);
|
||||
|
||||
child.Measure(new Size(cellWidth, cellHeight));
|
||||
}
|
||||
|
||||
// Calculate total size
|
||||
var totalWidth = _columnWidths.Sum() + Math.Max(0, columnCount - 1) * ColumnSpacing;
|
||||
var totalHeight = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing;
|
||||
|
||||
return new Size(
|
||||
totalWidth + Padding.Left + Padding.Right,
|
||||
totalHeight + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
|
||||
private int GetMaxRow()
|
||||
{
|
||||
int maxRow = 0;
|
||||
foreach (var pos in _childPositions.Values)
|
||||
{
|
||||
maxRow = Math.Max(maxRow, pos.Row + pos.RowSpan - 1);
|
||||
}
|
||||
return maxRow;
|
||||
}
|
||||
|
||||
private int GetMaxColumn()
|
||||
{
|
||||
int maxCol = 0;
|
||||
foreach (var pos in _childPositions.Values)
|
||||
{
|
||||
maxCol = Math.Max(maxCol, pos.Column + pos.ColumnSpan - 1);
|
||||
}
|
||||
return maxCol;
|
||||
}
|
||||
|
||||
private float[] CalculateSizesWithAuto(List<GridLength> definitions, float available, float spacing, int count, float[] naturalSizes)
|
||||
{
|
||||
if (count == 0) return new float[] { available };
|
||||
|
||||
var sizes = new float[count];
|
||||
var totalSpacing = Math.Max(0, count - 1) * spacing;
|
||||
var remainingSpace = available - totalSpacing;
|
||||
|
||||
// First pass: absolute and auto sizes
|
||||
float starTotal = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var def = i < definitions.Count ? definitions[i] : GridLength.Star;
|
||||
|
||||
if (def.IsAbsolute)
|
||||
{
|
||||
sizes[i] = def.Value;
|
||||
remainingSpace -= def.Value;
|
||||
}
|
||||
else if (def.IsAuto)
|
||||
{
|
||||
// Use natural size from measured children
|
||||
sizes[i] = naturalSizes[i];
|
||||
remainingSpace -= sizes[i];
|
||||
}
|
||||
else if (def.IsStar)
|
||||
{
|
||||
starTotal += def.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: star sizes (distribute remaining space)
|
||||
if (starTotal > 0 && remainingSpace > 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var def = i < definitions.Count ? definitions[i] : GridLength.Star;
|
||||
if (def.IsStar)
|
||||
{
|
||||
sizes[i] = (def.Value / starTotal) * remainingSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
private float GetCellWidth(int column, int span)
|
||||
{
|
||||
float width = 0;
|
||||
for (int i = column; i < Math.Min(column + span, _columnWidths.Length); i++)
|
||||
{
|
||||
width += _columnWidths[i];
|
||||
if (i > column) width += ColumnSpacing;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
private float GetCellHeight(int row, int span)
|
||||
{
|
||||
float height = 0;
|
||||
for (int i = row; i < Math.Min(row + span, _rowHeights.Length); i++)
|
||||
{
|
||||
height += _rowHeights[i];
|
||||
if (i > row) height += RowSpacing;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private float GetColumnOffset(int column)
|
||||
{
|
||||
float offset = 0;
|
||||
for (int i = 0; i < Math.Min(column, _columnWidths.Length); i++)
|
||||
{
|
||||
offset += _columnWidths[i] + ColumnSpacing;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
private float GetRowOffset(int row)
|
||||
{
|
||||
float offset = 0;
|
||||
for (int i = 0; i < Math.Min(row, _rowHeights.Length); i++)
|
||||
{
|
||||
offset += _rowHeights[i] + RowSpacing;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected override Rect ArrangeOverride(Rect bounds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
||||
|
||||
// Recalculate row heights for arrange bounds if they differ from measurement
|
||||
// This ensures Star rows expand to fill available space
|
||||
var rowCount = _rowHeights.Length > 0 ? _rowHeights.Length : 1;
|
||||
var columnCount = _columnWidths.Length > 0 ? _columnWidths.Length : 1;
|
||||
var arrangeRowHeights = _rowHeights;
|
||||
|
||||
// If we have arrange height and rows need recalculating
|
||||
if (content.Height > 0 && !float.IsInfinity(content.Height))
|
||||
{
|
||||
var measuredRowsTotal = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing;
|
||||
|
||||
// If arrange height is larger than measured, redistribute to Star rows
|
||||
if (content.Height > measuredRowsTotal + 1)
|
||||
{
|
||||
arrangeRowHeights = new float[rowCount];
|
||||
var extraHeight = content.Height - measuredRowsTotal;
|
||||
|
||||
// Count Star rows (implicit rows without definitions are Star)
|
||||
float totalStarWeight = 0;
|
||||
for (int i = 0; i < rowCount; i++)
|
||||
{
|
||||
var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star;
|
||||
if (def.IsStar) totalStarWeight += def.Value;
|
||||
}
|
||||
|
||||
// Distribute extra height to Star rows
|
||||
for (int i = 0; i < rowCount; i++)
|
||||
{
|
||||
var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star;
|
||||
arrangeRowHeights[i] = i < _rowHeights.Length ? _rowHeights[i] : 0;
|
||||
|
||||
if (def.IsStar && totalStarWeight > 0)
|
||||
{
|
||||
arrangeRowHeights[i] += extraHeight * (def.Value / totalStarWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
arrangeRowHeights = _rowHeights;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var pos = GetPosition(child);
|
||||
|
||||
var x = content.Left + GetColumnOffset(pos.Column);
|
||||
|
||||
// Calculate y using arrange row heights
|
||||
float y = content.Top;
|
||||
for (int i = 0; i < Math.Min(pos.Row, arrangeRowHeights.Length); i++)
|
||||
{
|
||||
y += arrangeRowHeights[i] + RowSpacing;
|
||||
}
|
||||
|
||||
var width = GetCellWidth(pos.Column, pos.ColumnSpan);
|
||||
|
||||
// Calculate height using arrange row heights
|
||||
float height = 0;
|
||||
for (int i = pos.Row; i < Math.Min(pos.Row + pos.RowSpan, arrangeRowHeights.Length); i++)
|
||||
{
|
||||
height += arrangeRowHeights[i];
|
||||
if (i > pos.Row) height += RowSpacing;
|
||||
}
|
||||
|
||||
// Clamp infinite dimensions
|
||||
if (float.IsInfinity(width) || float.IsNaN(width))
|
||||
width = content.Width;
|
||||
if (float.IsInfinity(height) || float.IsNaN(height) || height <= 0)
|
||||
height = content.Height;
|
||||
|
||||
// Apply child's margin
|
||||
var margin = child.Margin;
|
||||
var cellX = x + (float)margin.Left;
|
||||
var cellY = y + (float)margin.Top;
|
||||
var cellWidth = width - (float)margin.Left - (float)margin.Right;
|
||||
var cellHeight = height - (float)margin.Top - (float)margin.Bottom;
|
||||
|
||||
// Get child's desired size
|
||||
var childDesiredSize = child.Measure(new Size(cellWidth, cellHeight));
|
||||
var childWidth = (float)childDesiredSize.Width;
|
||||
var childHeight = (float)childDesiredSize.Height;
|
||||
|
||||
var vAlign = (int)child.VerticalOptions.Alignment;
|
||||
|
||||
// Apply HorizontalOptions
|
||||
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
||||
float finalX = cellX;
|
||||
float finalWidth = cellWidth;
|
||||
var hAlign = (int)child.HorizontalOptions.Alignment;
|
||||
if (hAlign != 3 && childWidth < cellWidth && childWidth > 0) // 3 = Fill
|
||||
{
|
||||
finalWidth = childWidth;
|
||||
if (hAlign == 1) // Center
|
||||
finalX = cellX + (cellWidth - childWidth) / 2;
|
||||
else if (hAlign == 2) // End
|
||||
finalX = cellX + cellWidth - childWidth;
|
||||
}
|
||||
|
||||
// Apply VerticalOptions
|
||||
float finalY = cellY;
|
||||
float finalHeight = cellHeight;
|
||||
// vAlign already calculated above for debug logging
|
||||
if (vAlign != 3 && childHeight < cellHeight && childHeight > 0) // 3 = Fill
|
||||
{
|
||||
finalHeight = childHeight;
|
||||
if (vAlign == 1) // Center
|
||||
finalY = cellY + (cellHeight - childHeight) / 2;
|
||||
else if (vAlign == 2) // End
|
||||
finalY = cellY + cellHeight - childHeight;
|
||||
}
|
||||
|
||||
child.Arrange(new Rect(finalX, finalY, finalWidth, finalHeight));
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SkiaGrid] EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.WriteLine($"[SkiaGrid] Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}");
|
||||
Console.WriteLine($"[SkiaGrid] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid position information.
|
||||
/// </summary>
|
||||
public readonly struct GridPosition
|
||||
{
|
||||
public int Row { get; }
|
||||
public int Column { get; }
|
||||
public int RowSpan { get; }
|
||||
public int ColumnSpan { get; }
|
||||
|
||||
public GridPosition(int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
Row = row;
|
||||
Column = column;
|
||||
RowSpan = Math.Max(1, rowSpan);
|
||||
ColumnSpan = Math.Max(1, columnSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid length specification.
|
||||
/// </summary>
|
||||
public readonly struct GridLength
|
||||
{
|
||||
public float Value { get; }
|
||||
public GridUnitType GridUnitType { get; }
|
||||
|
||||
public bool IsAbsolute => GridUnitType == GridUnitType.Absolute;
|
||||
public bool IsAuto => GridUnitType == GridUnitType.Auto;
|
||||
public bool IsStar => GridUnitType == GridUnitType.Star;
|
||||
|
||||
public static GridLength Auto => new(1, GridUnitType.Auto);
|
||||
public static GridLength Star => new(1, GridUnitType.Star);
|
||||
|
||||
public GridLength(float value, GridUnitType unitType = GridUnitType.Absolute)
|
||||
{
|
||||
Value = value;
|
||||
GridUnitType = unitType;
|
||||
}
|
||||
|
||||
public static GridLength FromAbsolute(float value) => new(value, GridUnitType.Absolute);
|
||||
public static GridLength FromStar(float value = 1) => new(value, GridUnitType.Star);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grid unit type options.
|
||||
/// </summary>
|
||||
public enum GridUnitType
|
||||
{
|
||||
Absolute,
|
||||
Star,
|
||||
Auto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute layout that positions children at exact coordinates.
|
||||
/// </summary>
|
||||
public class SkiaAbsoluteLayout : SkiaLayoutView
|
||||
{
|
||||
private readonly Dictionary<SkiaView, AbsoluteLayoutBounds> _childBounds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child at the specified position and size.
|
||||
/// </summary>
|
||||
public void AddChild(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
|
||||
{
|
||||
base.AddChild(child);
|
||||
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
|
||||
}
|
||||
|
||||
public override void RemoveChild(SkiaView child)
|
||||
{
|
||||
base.RemoveChild(child);
|
||||
_childBounds.Remove(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layout bounds for a child.
|
||||
/// </summary>
|
||||
public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child)
|
||||
{
|
||||
return _childBounds.TryGetValue(child, out var bounds)
|
||||
? bounds
|
||||
: new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the layout bounds for a child.
|
||||
/// </summary>
|
||||
public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
|
||||
{
|
||||
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
float maxRight = 0;
|
||||
float maxBottom = 0;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var layout = GetLayoutBounds(child);
|
||||
var bounds = layout.Bounds;
|
||||
|
||||
child.Measure(new Size(bounds.Width, bounds.Height));
|
||||
|
||||
maxRight = Math.Max(maxRight, bounds.Right);
|
||||
maxBottom = Math.Max(maxBottom, bounds.Bottom);
|
||||
}
|
||||
|
||||
return new Size(
|
||||
maxRight + Padding.Left + Padding.Right,
|
||||
maxBottom + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
|
||||
protected override Rect ArrangeOverride(Rect bounds)
|
||||
{
|
||||
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var layout = GetLayoutBounds(child);
|
||||
var childBounds = layout.Bounds;
|
||||
var flags = layout.Flags;
|
||||
|
||||
float x, y, width, height;
|
||||
|
||||
// X position
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.XProportional))
|
||||
x = content.Left + childBounds.Left * content.Width;
|
||||
else
|
||||
x = content.Left + childBounds.Left;
|
||||
|
||||
// Y position
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.YProportional))
|
||||
y = content.Top + childBounds.Top * content.Height;
|
||||
else
|
||||
y = content.Top + childBounds.Top;
|
||||
|
||||
// Width
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.WidthProportional))
|
||||
width = childBounds.Width * content.Width;
|
||||
else if (childBounds.Width < 0)
|
||||
width = (float)child.DesiredSize.Width;
|
||||
else
|
||||
width = childBounds.Width;
|
||||
|
||||
// Height
|
||||
if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional))
|
||||
height = childBounds.Height * content.Height;
|
||||
else if (childBounds.Height < 0)
|
||||
height = (float)child.DesiredSize.Height;
|
||||
else
|
||||
height = childBounds.Height;
|
||||
|
||||
// Apply child's margin
|
||||
var margin = child.Margin;
|
||||
var marginedBounds = new Rect(
|
||||
x + (float)margin.Left,
|
||||
y + (float)margin.Top,
|
||||
width - (float)margin.Left - (float)margin.Right,
|
||||
height - (float)margin.Top - (float)margin.Bottom);
|
||||
child.Arrange(marginedBounds);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute layout bounds for a child.
|
||||
/// </summary>
|
||||
public readonly struct AbsoluteLayoutBounds
|
||||
{
|
||||
public SKRect Bounds { get; }
|
||||
public AbsoluteLayoutFlags Flags { get; }
|
||||
|
||||
public AbsoluteLayoutBounds(SKRect bounds, AbsoluteLayoutFlags flags)
|
||||
{
|
||||
Bounds = bounds;
|
||||
Flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags for absolute layout positioning.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum AbsoluteLayoutFlags
|
||||
{
|
||||
None = 0,
|
||||
XProportional = 1,
|
||||
YProportional = 2,
|
||||
WidthProportional = 4,
|
||||
HeightProportional = 8,
|
||||
PositionProportional = XProportional | YProportional,
|
||||
SizeProportional = WidthProportional | HeightProportional,
|
||||
All = XProportional | YProportional | WidthProportional | HeightProportional
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -123,12 +124,12 @@ public class SkiaNavigationPage : SkiaView
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[SkiaNavigationPage] Push (no animation): setting _currentPage to " + page.Title);
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", "Push (no animation): setting _currentPage to " + page.Title);
|
||||
_currentPage = page;
|
||||
_currentPage.OnAppearing();
|
||||
Console.WriteLine("[SkiaNavigationPage] Push: calling Invalidate");
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", "Push: calling Invalidate");
|
||||
Invalidate();
|
||||
Console.WriteLine("[SkiaNavigationPage] Push: Invalidate called, _currentPage is now " + _currentPage?.Title);
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", "Push: Invalidate called, _currentPage is now " + _currentPage?.Title);
|
||||
}
|
||||
|
||||
Pushed?.Invoke(this, new NavigationEventArgs(page));
|
||||
@@ -326,7 +327,7 @@ public class SkiaNavigationPage : SkiaView
|
||||
else if (_currentPage != null)
|
||||
{
|
||||
// Draw current page normally
|
||||
Console.WriteLine("[SkiaNavigationPage] OnDraw: drawing _currentPage=" + _currentPage.Title);
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", "OnDraw: drawing _currentPage=" + _currentPage.Title);
|
||||
_currentPage.Bounds = new Rect(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
|
||||
_currentPage.Draw(canvas);
|
||||
|
||||
@@ -375,7 +376,7 @@ public class SkiaNavigationPage : SkiaView
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaNavigationPage] OnPointerPressed at ({e.X}, {e.Y}), _isAnimating={_isAnimating}");
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", $"OnPointerPressed at ({e.X}, {e.Y}), _isAnimating={_isAnimating}");
|
||||
if (_isAnimating) return;
|
||||
|
||||
// Check for back button click
|
||||
@@ -383,13 +384,13 @@ public class SkiaNavigationPage : SkiaView
|
||||
{
|
||||
if (e.X < 56 && e.Y < _navigationBarHeight)
|
||||
{
|
||||
Console.WriteLine($"[SkiaNavigationPage] Back button clicked");
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", "Back button clicked");
|
||||
Pop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[SkiaNavigationPage] Forwarding to _currentPage: {_currentPage?.GetType().Name}");
|
||||
DiagnosticLog.Debug("SkiaNavigationPage", $"Forwarding to _currentPage: {_currentPage?.GetType().Name}");
|
||||
_currentPage?.OnPointerPressed(e);
|
||||
}
|
||||
|
||||
@@ -454,7 +455,7 @@ public class SkiaNavigationPage : SkiaView
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SkiaNavigationPage] HitTest error: {ex.Message}");
|
||||
DiagnosticLog.Error("SkiaNavigationPage", $"HitTest error: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
@@ -204,7 +205,7 @@ public class SkiaPage : SkiaView
|
||||
var availableSize = new Size(adjustedBounds.Width, adjustedBounds.Height);
|
||||
_content.Measure(availableSize);
|
||||
_content.Arrange(new Rect(adjustedBounds.Left, adjustedBounds.Top, adjustedBounds.Width, adjustedBounds.Height));
|
||||
Console.WriteLine($"[SkiaPage] Drawing content: {_content.GetType().Name}, Bounds={_content.Bounds}, IsVisible={_content.IsVisible}");
|
||||
DiagnosticLog.Debug("SkiaPage", $"Drawing content: {_content.GetType().Name}, Bounds={_content.Bounds}, IsVisible={_content.IsVisible}");
|
||||
_content.Draw(canvas);
|
||||
}
|
||||
|
||||
@@ -284,7 +285,7 @@ public class SkiaPage : SkiaView
|
||||
|
||||
public void OnAppearing()
|
||||
{
|
||||
Console.WriteLine($"[SkiaPage] OnAppearing called for: {Title}, HasListeners={Appearing != null}");
|
||||
DiagnosticLog.Debug("SkiaPage", $"OnAppearing called for: {Title}, HasListeners={Appearing != null}");
|
||||
Appearing?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -436,7 +437,7 @@ public class SkiaContentPage : SkiaPage
|
||||
private void DrawToolbarItems(SKCanvas canvas, SKRect navBarBounds)
|
||||
{
|
||||
var primaryItems = _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary).ToList();
|
||||
Console.WriteLine($"[SkiaContentPage] DrawToolbarItems: {primaryItems.Count} primary items, navBarBounds={navBarBounds}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"DrawToolbarItems: {primaryItems.Count} primary items, navBarBounds={navBarBounds}");
|
||||
if (primaryItems.Count == 0) return;
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, 14);
|
||||
@@ -470,7 +471,7 @@ public class SkiaContentPage : SkiaPage
|
||||
var destRect = new SKRect(iconX, iconY, iconX + iconSize, iconY + iconSize);
|
||||
canvas.DrawBitmap(item.Icon, destRect);
|
||||
|
||||
Console.WriteLine($"[SkiaContentPage] Drew toolbar icon '{item.Text}' at ({iconX}, {iconY})");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"Drew toolbar icon '{item.Text}' at ({iconX}, {iconY})");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -490,33 +491,33 @@ public class SkiaContentPage : SkiaPage
|
||||
canvas.DrawText(item.Text, x, y, textPaint);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}");
|
||||
rightEdge = itemLeft - 8; // Gap between items
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaContentPage] OnPointerPressed at ({e.X}, {e.Y}), ShowNavigationBar={ShowNavigationBar}, NavigationBarHeight={NavigationBarHeight}");
|
||||
Console.WriteLine($"[SkiaContentPage] ToolbarItems count: {_toolbarItems.Count}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"OnPointerPressed at ({e.X}, {e.Y}), ShowNavigationBar={ShowNavigationBar}, NavigationBarHeight={NavigationBarHeight}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"ToolbarItems count: {_toolbarItems.Count}");
|
||||
|
||||
// Check toolbar item clicks
|
||||
if (ShowNavigationBar && e.Y < NavigationBarHeight)
|
||||
{
|
||||
Console.WriteLine($"[SkiaContentPage] In navigation bar area, checking toolbar items");
|
||||
DiagnosticLog.Debug("SkiaContentPage", "In navigation bar area, checking toolbar items");
|
||||
foreach (var item in _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary))
|
||||
{
|
||||
var bounds = item.HitBounds;
|
||||
var contains = bounds.Contains(e.X, e.Y);
|
||||
Console.WriteLine($"[SkiaContentPage] Checking item '{item.Text}', HitBounds=({bounds.Left},{bounds.Top},{bounds.Right},{bounds.Bottom}), Click=({e.X},{e.Y}), Contains={contains}, Command={item.Command != null}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"Checking item '{item.Text}', HitBounds=({bounds.Left},{bounds.Top},{bounds.Right},{bounds.Bottom}), Click=({e.X},{e.Y}), Contains={contains}, Command={item.Command != null}");
|
||||
if (contains)
|
||||
{
|
||||
Console.WriteLine($"[SkiaContentPage] Toolbar item clicked: {item.Text}");
|
||||
DiagnosticLog.Debug("SkiaContentPage", $"Toolbar item clicked: {item.Text}");
|
||||
item.Command?.Execute(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"[SkiaContentPage] No toolbar item hit");
|
||||
DiagnosticLog.Debug("SkiaContentPage", "No toolbar item hit");
|
||||
}
|
||||
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -428,7 +429,7 @@ public class SkiaScrollView : SkiaView
|
||||
|
||||
public override void OnScroll(ScrollEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaScrollView] OnScroll - DeltaY={e.DeltaY}, ScrollableHeight={ScrollableHeight}, ContentSize={ContentSize}, Bounds={Bounds}");
|
||||
DiagnosticLog.Debug("SkiaScrollView", $"OnScroll - DeltaY={e.DeltaY}, ScrollableHeight={ScrollableHeight}, ContentSize={ContentSize}, Bounds={Bounds}");
|
||||
|
||||
// Handle mouse wheel scrolling
|
||||
var deltaMultiplier = 40f; // Scroll speed
|
||||
@@ -438,7 +439,7 @@ public class SkiaScrollView : SkiaView
|
||||
{
|
||||
var oldScrollY = _scrollY;
|
||||
ScrollY += e.DeltaY * deltaMultiplier;
|
||||
Console.WriteLine($"[SkiaScrollView] ScrollY changed: {oldScrollY} -> {_scrollY}");
|
||||
DiagnosticLog.Debug("SkiaScrollView", $"ScrollY changed: {oldScrollY} -> {_scrollY}");
|
||||
if (_scrollY != oldScrollY)
|
||||
scrolled = true;
|
||||
}
|
||||
@@ -876,7 +877,7 @@ public class SkiaScrollView : SkiaView
|
||||
var actualBounds = bounds;
|
||||
if (double.IsInfinity(bounds.Height) || double.IsNaN(bounds.Height))
|
||||
{
|
||||
Console.WriteLine($"[SkiaScrollView] WARNING: Infinite/NaN height, using default viewport={DefaultViewportHeight}");
|
||||
DiagnosticLog.Warn("SkiaScrollView", $"Infinite/NaN height, using default viewport={DefaultViewportHeight}");
|
||||
actualBounds = new Rect(bounds.Left, bounds.Top, bounds.Width, DefaultViewportHeight);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user