18 Commits

Author SHA1 Message Date
c6a3f4acc4 chore(perf): bump version to 9.0.40 to match MAUI 9.0.40
All checks were successful
CI / Build (Linux) (push) Successful in 21s
Release / Build and Publish (push) Successful in 20s
Update version from 9.0.0 to 9.0.40 to align precisely with MAUI 9.0.40 release. Update CHANGELOG, package metadata, and templates. Fix thread safety issue in FontFallbackManager by changing _glyphCache from Dictionary to ConcurrentDictionary to prevent race conditions during concurrent glyph lookups.
2026-03-07 01:48:58 -05:00
8a5ad459ed Merge pull request 'chore(perf): realign version to 9.0.0 to match .NET/MAUI versioning' (#1) from dotNet9 into main
All checks were successful
CI / Build (Linux) (push) Successful in 18s
Reviewed-on: #1
2026-03-07 05:16:59 +00:00
840ad0ce8f chore(perf): realign version to 9.0.0 to match .NET/MAUI versioning
All checks were successful
CI / Build (Linux) (pull_request) Successful in 23s
Change version from 1.0.0 to 9.0.0 to align with .NET 9 / MAUI 9.0.x versioning convention. Update all package references, templates, and documentation. Change copyright holder from "MarketAlly LLC" to "MarketAlly Pte Ltd" across all files. Update CHANGELOG to document version realignment and mark 1.0.0 as deprecated. Update release notes to reflect 541 passing tests (up from 217) and include native resource leak fixes, SafeHandle wrappers, and performance benchmarks.
2026-03-07 00:14:47 -05:00
967713c01a test(perf): add performance benchmarks for rendering pipeline
All checks were successful
CI / Build (Linux) (push) Successful in 19s
Add performance benchmark tests for critical rendering paths including Measure/Arrange operations on flat layouts (100 children), deep nesting (20 levels), and Grid layouts (10x10). Include HitTest performance validation. Tests use Stopwatch with generous upper bounds to catch regressions while avoiding flaky failures on slow CI machines. Benchmarks verify operations complete within acceptable time budgets (5-10ms thresholds).
2026-03-06 23:43:41 -05:00
6b45f28b4e feat(interop): add safe handle wrappers and configuration options
Implement SafeHandle wrappers for native resources (SafeGtkWidgetHandle, SafeGObjectHandle, SafeX11DisplayHandle, SafeX11CursorHandle, SafeDlopenHandle) to prevent memory leaks. Move gesture and rendering configuration from static properties to LinuxApplicationOptions for better testability and DI compatibility. Add THREADING.md and DI-MIGRATION.md documentation. Include LayoutIntegrationTests for Measure/Arrange pipeline and SkiaViewTheoryTests with parameterized test cases using [Theory] attributes.
2026-03-06 23:40:51 -05:00
3412cb982e fix(interop): resolve native resource leaks in GTK and WebKit interop
All checks were successful
CI / Build (Linux) (push) Successful in 21s
Fix critical memory leaks identified in architecture review: Add signal handler disconnection in WebKitNative (load-changed and script-dialog signals now properly cleaned up), implement GTK idle callback cleanup with automatic removal on completion, add dlclose() calls for WebKit library handles, track GTK signal IDs in GtkSkiaSurfaceWidget for proper disposal. Replace empty catch blocks in GestureManager with logged exception handling. Add WebKitNative.Cleanup() and GtkNative.ClearCallbacks() methods for application shutdown.
2026-03-06 23:14:53 -05:00
c5221ba580 test: add unit tests for controls and rendering helpers
All checks were successful
CI / Build (Linux) (push) Successful in 18s
Add comprehensive test coverage for 20+ Skia controls including ActivityIndicator, Border, CheckBox, CollectionView, DatePicker, Editor, Grid, Image, ImageButton, Label, NavigationPage, Page, Picker, ProgressBar, RadioButton, SearchBar, Stepper, Switch, and TimePicker. Include tests for TextRenderingHelper utility methods covering color conversion, font style mapping, and font family resolution.
2026-03-06 22:43:25 -05:00
077abc2feb refactor: split large files into partial classes by concern
Split LinuxApplication into Input and Lifecycle partials. Extract SkiaView into Accessibility, Drawing, and Input partials. Split SkiaEntry and SkiaEditor into Drawing and Input partials. Extract TextRenderingHelper from SkiaRenderingEngine. Create dedicated files for SkiaAbsoluteLayout, SkiaGrid, and SkiaStackLayout. This reduces file sizes from 40K+ lines to manageable units organized by responsibility.
2026-03-06 22:36:23 -05:00
ee60b983a4 docs: remove --prerelease flag from package install commands 2026-03-06 22:13:17 -05:00
0fb0051a24 docs: add changelog for v1.0.0 release
Document complete feature set including 35+ Skia-rendered controls, full XAML support, X11/Wayland display servers, accessibility, platform services, gesture recognition, and project templates. Include fixes for GestureManager memory leak, text binding recursion, and rendering pipeline crash protection.
2026-03-06 22:11:54 -05:00
15ced2ac55 chore: bump version to 1.0.0 and update release metadata
Update package references from RC/preview versions to stable 1.0.* across all templates and project files. Mark RC1 roadmap as completed and update documentation to reflect v1.0.0 release. Add GestureManager cleanup in SkiaView.Dispose() to prevent memory leaks. Update copyright years to 2025-2026 and add development artifacts to .gitignore.
2026-03-06 22:11:46 -05:00
e55230c441 refactor: replace Console.WriteLine with DiagnosticLog service
All checks were successful
CI / Build (Linux) (push) Successful in 21s
Replace 495+ Console.WriteLine debug statements across handlers, dispatching, services, views, and window components with centralized DiagnosticLog service for proper logging infrastructure. Add new DiagnosticLog.cs service with Debug/Error methods to eliminate debug logging pollution in production code.
2026-03-06 22:06:08 -05:00
08e0c4d2b9 Create note-1770523035702-znjlto6vn.json
All checks were successful
CI / Build (Linux) (push) Successful in 25s
2026-02-15 23:57:39 -05:00
d9d3218f17 docs(ci): add OpenMaui technical article series
Add 7-part article series covering OpenMaui Linux implementation deep dives:

1. SkiaSharp rendering architecture and performance
2. Window management across X11, Wayland, and GTK backends
3. Advanced text rendering with IME, font fallback, and internationalization
4. Performance optimization strategies for rendering and memory management
5. Custom control development patterns
6. Dirty region tracking and invalidation
7. HarfBuzz integration and CJK language support

Total 389 lines of technical documentation covering rendering pipelines, platform abstraction, and optimization techniques.
2026-01-30 00:50:26 -05:00
996ffb67c0 fix(ci): use tag-based trigger for release workflow
All checks were successful
CI / Build (Linux) (push) Successful in 18s
Release / Build and Publish (push) Successful in 20s
Remove release event trigger in favor of tag push trigger only. Simplify version extraction to use gitea.ref_name directly instead of parsing GITEA_REF environment variable.

This ensures the workflow runs immediately when a version tag is pushed, without requiring a separate release creation step.
2026-01-29 23:33:28 -05:00
4d880e77d5 refactor(ci): simplify CI workflow to Linux-only builds
Some checks failed
Release / Build and Publish (release) Failing after 21s
CI / Build (Linux) (push) Successful in 22s
Release / Build and Publish (push) Failing after 18s
Remove Windows and macOS build jobs from CI workflow since this is a Linux-specific MAUI implementation. Consolidate artifact uploads to single "nuget-packages" artifact.

Fix release workflow to use Gitea-specific variables (GITEA_REF, gitea.repository_owner) instead of GitHub equivalents, and use GITEATOKEN secret for package publishing.
2026-01-29 23:29:51 -05:00
ade3a8a47d Move CLAUDE.md to vault
All checks were successful
CI / Build (Linux) (push) Successful in 23s
CI / Build (macOS) (push) Successful in 31s
CI / Build (Windows) (push) Successful in 9h0m12s
2026-01-30 03:49:11 +00:00
b7424194f4 Workflows set
Some checks failed
CI / Build (macOS) (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled
CI / Build (Linux) (push) Has been cancelled
2026-01-24 07:53:30 +00:00
153 changed files with 11968 additions and 5599 deletions

View File

@@ -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

View File

@@ -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
View File

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

View 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": []
}

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

58
CHANGELOG.md Normal file
View 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
View File

@@ -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">&#xe145;</x:String>
<x:String x:Key="IconDelete">&#xe872;</x:String>
<x:String x:Key="IconSave">&#xe161;</x:String>
<x:String x:Key="IconCheck">&#xe876;</x:String>
<x:String x:Key="IconEdit">&#xe3c9;</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

View 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.

View File

@@ -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;
});

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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)
{

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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)
{

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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))}...");
}
}

View File

@@ -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");
}
}

View File

@@ -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}");
}
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
View 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;
}
}

View 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;
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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
View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -5,7 +5,7 @@ A comprehensive Linux platform implementation for .NET MAUI using SkiaSharp rend
[![NuGet](https://img.shields.io/nuget/v/OpenMaui.Controls.Linux)](https://www.nuget.org/packages/OpenMaui.Controls.Linux)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}

View 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;
}
}

View File

@@ -99,8 +99,9 @@ public class AppInfoService : IAppInfo
UseShellExecute = true
});
}
catch
catch (Exception ex)
{
DiagnosticLog.Debug("AppInfoService", "Settings launch fallback failed", ex);
}
}
}

View File

@@ -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>

View File

@@ -129,8 +129,9 @@ public class ConnectivityService : IConnectivity, IDisposable
}
}
}
catch
catch (Exception ex)
{
DiagnosticLog.Debug("ConnectivityService", "Gateway check failed", ex);
}
return false;
}

View File

@@ -137,8 +137,9 @@ public class DeviceDisplayService : IDeviceDisplay
});
}
}
catch
catch (Exception ex)
{
DiagnosticLog.Debug("DeviceDisplayService", "Display info refresh failed", ex);
}
}

View File

@@ -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
View 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}");
}
}

View File

@@ -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);
}

View File

@@ -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))
{

View File

@@ -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))
{

View File

@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using 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()
{

View File

@@ -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}");
}
}
}

View File

@@ -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;

View File

@@ -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}");
}
}
}

View File

@@ -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}");
}
}

View File

@@ -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;
}

View File

@@ -464,7 +464,7 @@ public class HiDpiService
return textScale;
}
}
catch { }
catch (Exception ex) { DiagnosticLog.Debug("HiDpiService", "Font scale factor detection failed", ex); }
return _scaleFactor;
}

View File

@@ -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}");
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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 "";
}
}

View File

@@ -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}");
}
}

View File

@@ -134,7 +134,7 @@ public class SystemTrayService : IDisposable
}
}
}
catch { }
catch (Exception ex) { DiagnosticLog.Debug("SystemTrayService", "Tray output reading failed", ex); }
});
return Task.FromResult(true);

View File

@@ -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()

View File

@@ -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)

View File

@@ -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();

View File

@@ -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
View 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
}

View File

@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.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

View File

@@ -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;
}

View File

@@ -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
View 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
View 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);
}
}

View File

File diff suppressed because it is too large Load Diff

303
Views/SkiaEntry.Drawing.cs Normal file
View 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
View 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);
}
}

View File

File diff suppressed because it is too large Load Diff

500
Views/SkiaGrid.cs Normal file
View 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
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}
});

View File

@@ -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)
{

View File

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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