126 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
0f093bdb1e Update X11Window.cs
Some checks failed
CI / Build and Test (push) Failing after 8h0m10s
2026-01-24 07:28:36 +00:00
675673f026 Fixes for taskbar app 2026-01-24 06:55:59 +00:00
f1e3630d1b Fixes for GTK - still wrong 2026-01-24 06:13:13 +00:00
38c48fc99f Remove the old samples 2026-01-24 05:16:29 +00:00
f48f909ee2 Update .gitignore 2026-01-24 03:18:50 +00:00
433787ff80 Image fixes 2026-01-24 03:18:08 +00:00
ed89b6494d Fixes for pages 2026-01-24 02:37:37 +00:00
7830356f24 Fix labels, lists and other tweaks 2026-01-24 02:21:56 +00:00
5415c71e9e Update ButtonHandler.cs 2026-01-24 02:00:00 +00:00
2c5036596e Removed duplicate 2026-01-24 01:59:53 +00:00
0c2508d715 Fixes for handlers 2026-01-24 01:53:26 +00:00
f4422d4af1 cursor blink in entry/editor 2026-01-17 15:26:13 +00:00
88679dfae8 context menu fixes 2026-01-17 15:19:03 +00:00
10a66cd399 More fixes 2026-01-17 15:06:39 +00:00
9451611c3a control work fixes 2026-01-17 08:51:13 +00:00
dc52f7f2bc More fixes 2026-01-17 08:06:22 +00:00
f1a368a6c2 templates updated 2026-01-17 05:31:40 +00:00
ad12779b73 Docs added 2026-01-17 05:27:21 +00:00
7d2ac327a3 More fixes 2026-01-17 05:22:37 +00:00
f62d4aa5f2 Missing maui compliance 2026-01-17 03:45:05 +00:00
a367365ce5 Color issues 2026-01-17 03:36:37 +00:00
aad915ad86 Missing backgroundcolor 2026-01-17 03:10:29 +00:00
5443c7c22a Gesture support 2026-01-17 02:48:59 +00:00
5a915ca06a Window management 2026-01-17 02:33:00 +00:00
47a5fc8c01 Missing items 2026-01-17 02:23:05 +00:00
523de9d8b9 Update LinuxMauiAppBuilderExtensions.cs 2026-01-17 01:47:26 +00:00
b07228922f Missing bindings defaults 2026-01-17 01:43:42 +00:00
4c70118be6 Create MenuBarHandler.cs 2026-01-17 01:22:52 +00:00
868ce1fcae CollectionView completed 2026-01-17 01:18:57 +00:00
b1749b1347 Page completed 2026-01-17 01:18:35 +00:00
d8bdf5472f Navigation completed 2026-01-17 01:17:36 +00:00
7a1241cbf2 Layout 2026-01-17 00:46:27 +00:00
27e2dc7136 Update SkiaPicker.cs 2026-01-16 05:51:54 +00:00
8b1c733943 Border completed 2026-01-16 05:49:20 +00:00
331d6839d9 General renderers/views 2026-01-16 05:42:21 +00:00
bf2f380f56 WebView completed 2026-01-16 05:40:49 +00:00
a11b081510 more searchBar 2026-01-16 05:39:10 +00:00
c27f073938 Picker completed 2026-01-16 05:38:45 +00:00
59bcb9244e TimePicker completed 2026-01-16 05:36:36 +00:00
c21ff803fe DatePicker completed 2026-01-16 05:36:22 +00:00
7056ab7e4e SearchBar completed 2026-01-16 05:31:38 +00:00
1b1619de06 Frame completed 2026-01-16 05:31:21 +00:00
538d4bad65 BoxView completed 2026-01-16 05:31:09 +00:00
85b3c22dc7 Border completed 2026-01-16 05:30:52 +00:00
675466a0f5 TimePicker 2026-01-16 05:14:14 +00:00
870382097b DatePicker 2026-01-16 05:14:04 +00:00
20fcbd78e3 Picker conmpleted 2026-01-16 05:10:40 +00:00
0094cdef45 Progressbar completed 2026-01-16 05:04:05 +00:00
8f1eba7fbe Activity completed 2026-01-16 05:03:49 +00:00
436c9d60cb Stepper completed 2026-01-16 05:01:27 +00:00
083f110cf4 Slider completed 2026-01-16 04:57:40 +00:00
f263ee96b3 Radio button 2026-01-16 04:56:23 +00:00
271f8d7fa9 Switch 2026-01-16 04:54:18 +00:00
a8c8939a3f Checkbox 2026-01-16 04:54:03 +00:00
71a37da1a4 Image and ImageButton 2026-01-16 04:49:21 +00:00
81f7d9c90e Entry 2026-01-16 04:40:02 +00:00
d5a7560479 Editor and Search 2026-01-16 04:39:50 +00:00
209c56e592 Label completed 2026-01-16 04:23:47 +00:00
9a49185183 Button completed 2026-01-16 04:14:34 +00:00
a2800464c8 Update SkiaView.cs 2026-01-16 03:57:31 +00:00
aab53ee919 Initial start 2026-01-16 03:40:47 +00:00
4a64927c12 Removed samples 2026-01-11 10:56:09 -05:00
bc80436a34 Add DialogsPage and MoreControlsPage to ShellDemo
DialogsPage demonstrates:
- Alert dialogs (simple, confirmation)
- Action sheets (with destructive option)
- Input prompts (text, numeric)
- File pickers (single, multiple, images)
- Folder picker

MoreControlsPage demonstrates:
- Stepper (basic and custom range)
- RadioButton (vertical and horizontal groups)
- Image placeholders with aspect modes
- Clipboard (copy/paste)
- Share and Launcher services
- BoxView shapes and dividers

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 20:05:50 -05:00
01270c6938 Add ShellDemo sample with comprehensive XAML controls showcase
Complete ShellDemo application demonstrating all MAUI controls:
- App/AppShell: Shell navigation with flyout menu
- HomePage: Feature cards, theme toggle, quick actions
- ButtonsPage: Button styles, states, variations, event logging
- TextInputPage: Entry, Editor, SearchBar with keyboard shortcuts
- SelectionPage: CheckBox, Switch, Slider with colored variants
- PickersPage: Picker, DatePicker, TimePicker demos
- ListsPage: CollectionView with fruits, colors, contacts
- ProgressPage: ProgressBar, ActivityIndicator, interactive demo
- GridsPage: Grid layouts - auto/star/absolute sizing, spans, nesting
- AboutPage: OpenMaui Linux information
- DetailPage: Push/pop navigation demo

All pages use proper XAML with code-behind following MAUI patterns.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 20:02:24 -05:00
18ab0abe97 Add TodoApp sample with reconstructed XAML
Complete TodoApp sample application with:
- App.xaml/cs: Colors and styles for light/dark themes
- TodoListPage: Task list with theme toggle switch
- NewTodoPage: Form to create new tasks
- TodoDetailPage: Edit task details with delete option
- TodoItem.cs/TodoService.cs: Data model and service
- SVG icons for save, delete, and add actions

Theme switching via toggle on main page applies app-wide.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 19:52:56 -05:00
8a36a44341 Reconstruct XamlBrowser sample with XAML from decompiled code
Created complete XamlBrowser sample application:
- App.xaml: Colors and styles for light/dark theme support
- App.xaml.cs: BrowserApp with ToggleTheme()
- MainPage.xaml: Toolbar (Back, Forward, Refresh, Stop, Home),
  address bar, Go button, WebView, status bar with theme toggle
- MainPage.xaml.cs: Navigation logic, URL handling, progress animation
- MauiProgram.cs: UseLinuxPlatform() configuration
- Program.cs: LinuxProgramHost entry point
- Resources/Images: 10 SVG icons for toolbar (dark/light variants)

UI matches screenshot provided by user:
- Dark gray toolbar with navigation buttons
- Entry field for URL with rounded corners
- Green "Go" button
- WebView displaying content
- Status bar with theme toggle

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 18:32:08 -05:00
95e7d0c90b Add CLAUDE.md for XAML reconstruction project
Documents the plan to reconstruct XAML files from decompiled sample apps:
- ShellDemo: 12 files (App, AppShell, 10 pages)
- TodoApp: 4 files (App, 3 pages)
- XamlBrowser: 2 files (App, MainPage)

Includes:
- Color values extracted from decompiled code
- Style definitions
- AppShell structure with FlyoutItems
- Key patterns for converting decompiled C# to XAML
- Workflow and tracking checklist

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:48:18 -05:00
c60453ea31 Fix incomplete functionality, nullable warnings, and async issues
Incomplete functionality fixes:
- SkiaEditor: Wire up Completed event to fire on focus lost
- X11InputMethodService: Remove unused _commitCallback field
- SkiaWebView: Set _isProperlyReparented when reparenting succeeds,
  use _lastMainX/_lastMainY to track main window position,
  add _isEmbedded guard to prevent double embedding

Nullable reference fixes:
- Easing: Reorder BounceOut before BounceIn (static init order)
- GestureManager: Use local command variable instead of re-accessing
- SkiaShell: Handle null Title with ?? operator
- GLibNative: Use null! for closure pattern
- LinuxProgramHost: Default title if null
- SkiaWebView.LoadHtml: Add null/empty check for html
- SystemThemeService: Initialize Colors with default values
- DeviceDisplayService/AppInfoService: Use var for nullable env vars
- EmailService: Add null check for message parameter

Async fixes:
- SkiaImage: Use _ = for fire-and-forget async calls
- SystemTrayService: Convert async method without await to sync Task

Reduces warnings from 156 to 133 (remaining are P/Invoke structs
and obsolete MAUI API usage)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:20:11 -05:00
7b22d67920 Fix BorderHandler and FrameHandler ConnectHandler/DisconnectHandler
- Add ConnectHandler with MauiView property and Tapped event subscription
- Add DisconnectHandler to cleanup event subscription and MauiView
- Add OnPlatformViewTapped to call GestureManager.ProcessTap
- These changes match the decompiled production code
- Update MERGE_TRACKING.md to mark both handlers as complete

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:08:52 -05:00
f6eadaad57 Verify Views files against decompiled, extract embedded types
Fixed files:
- SkiaImageButton.cs: Added SVG support with multi-path search
- SkiaNavigationPage.cs: Added LinuxApplication.IsGtkMode check
- SkiaRefreshView.cs: Added ICommand support (Command, CommandParameter)
- SkiaTemplatedView.cs: Added missing using statements

Extracted embedded types to separate files (matching decompiled pattern):
- From SkiaMenuBar.cs: MenuBarItem, MenuItem, SkiaMenuFlyout, MenuItemClickedEventArgs
- From SkiaNavigationPage.cs: NavigationEventArgs
- From SkiaTabbedPage.cs: TabItem
- From SkiaVisualStateManager.cs: SkiaVisualStateGroupList, SkiaVisualStateGroup, SkiaVisualState, SkiaVisualStateSetter
- From SkiaSwipeView.cs: SwipeItem, SwipeStartedEventArgs, SwipeEndedEventArgs
- From SkiaFlyoutPage.cs: FlyoutLayoutBehavior (already separate)
- From SkiaIndicatorView.cs: IndicatorShape (already separate)
- From SkiaBorder.cs: SkiaFrame
- From SkiaCarouselView.cs: PositionChangedEventArgs
- From SkiaCollectionView.cs: SkiaSelectionMode, ItemsLayoutOrientation
- From SkiaContentPresenter.cs: LayoutAlignment

Verified matching decompiled:
- SkiaContextMenu.cs, SkiaFlexLayout.cs, SkiaGraphicsView.cs

Build: 0 errors

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:02:39 -05:00
6007b84e7a Fix BindingModes: SkiaLayoutView, SkiaStackLayout, SkiaGrid
- SkiaLayoutView: Spacing, Padding, ClipToBounds
- SkiaStackLayout: Orientation
- SkiaGrid: RowSpacing, ColumnSpacing

All now TwoWay to match decompiled production.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:30:17 -05:00
4d225a43ef Fix BindingModes: SkiaLabel, SkiaScrollView
- SkiaLabel.cs: Added TwoWay to VerticalTextAlignment, LineBreakMode,
  MaxLines, LineHeight, CharacterSpacing, Padding
- SkiaScrollView.cs: Added TwoWay to Orientation, HorizontalScrollBarVisibility,
  VerticalScrollBarVisibility, ScrollBarColor, ScrollBarWidth

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:29:10 -05:00
55d4a6eaad Fix Views: SkiaEntry, SkiaEditor, SkiaShell, SkiaWebView
- SkiaEntry.cs: TextProperty BindingMode.OneWay (was TwoWay)
- SkiaEditor.cs: All BindingModes corrected (Text=OneWay, others=TwoWay)
- SkiaShell.cs: Added FlyoutTextColor, ContentBackgroundColor properties,
  route registration system, query parameter support, OnScroll handler
- SkiaWebView.cs: Full rewrite with X11 embedding, GTK window positioning,
  hardware acceleration settings, load-changed callbacks, position tracking

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:27:33 -05:00
5613df6031 Verify remaining handlers: GestureManager, GtkWebViewManager, GtkWebViewPlatformView
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:11:38 -05:00
d34f4e1fea Update MERGE_TRACKING.md - all handlers verified
Verified against decompiled production:
- ApplicationHandler, CollectionViewHandler, FlexLayoutHandler
- FlyoutPageHandler, GraphicsViewHandler, ItemsViewHandler
- WebViewHandler

All handlers now verified or fixed.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:10:19 -05:00
a471cb071a Update MERGE_TRACKING.md - verify more handlers
Verified against decompiled production:
- BoxViewHandler.cs
- LayoutHandler.cs (+ StackLayoutHandler, GridHandler)
- PageHandler.cs (+ ContentPageHandler)
- WindowHandler.cs (+ SkiaWindow)
- ShellHandler.cs
- NavigationPageHandler.cs
- TabbedPageHandler.cs

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:02:51 -05:00
317aaaf23c Verify and fix handlers against decompiled production
- ImageHandler.Linux.cs: Verified matches production
- ScrollViewHandler.Linux.cs: Verified matches production
- StepperHandler.Linux.cs: Verified matches production
- RadioButtonHandler.Linux.cs: Verified matches production
- SearchBarHandler.Linux.cs: Fixed namespace, added CancelButtonColor, SolidPaint, null checks
- ImageButtonHandler.cs: Verified matches production
- DatePickerHandler.cs: Verified matches production
- TimePickerHandler.cs: Verified matches production

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 13:59:47 -05:00
6f0d10935c Fix handlers to match decompiled production code
- ButtonHandler: Removed MapText/TextColor/Font (not in production), fixed namespace
- LabelHandler: Added CharacterSpacing/LayoutAlignment/FormattedText, ConnectHandler gesture logic
- EntryHandler: Added CharacterSpacing/ClearButtonVisibility/VerticalTextAlignment
- EditorHandler: Created from decompiled (was missing)
- SliderHandler: Fixed namespace, added ConnectHandler init calls
- SwitchHandler: Added OffTrackColor logic, fixed namespace
- CheckBoxHandler: Added VerticalLayoutAlignment/HorizontalLayoutAlignment
- ProgressBarHandler: Added ConnectHandler/DisconnectHandler IsVisible tracking
- PickerHandler: Created from decompiled with collection changed tracking
- ActivityIndicatorHandler: Removed IsEnabled/BackgroundColor (not in production)
- All handlers now use namespace Microsoft.Maui.Platform.Linux.Handlers
- All handlers have proper null checks on PlatformView
- Updated MERGE_TRACKING.md with accurate status

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 13:51:12 -05:00
fd9043f749 Fix GestureManager, CollectionViewHandler from decompiled production
GestureManager.cs:
- Added third fallback for TappedEvent fields (_TappedHandler, <Tapped>k__BackingField)
- Added type info dump when event cannot be fired (debugging aid)
- Fixed swipe Right handling with proper direction check
- Added SendSwiped success log
- Changed Has* methods to use foreach instead of LINQ

CollectionViewHandler.cs:
- Added full OnItemTapped implementation with gesture handling
- Added MauiView assignment in MapItemTemplate for gesture processing

SkiaItemsView.cs:
- Added GetItemView() method for CollectionViewHandler

Verified handlers match decompiled:
- GraphicsViewHandler
- ItemsViewHandler
- WindowHandler

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 13:05:16 -05:00
b0b3746968 Fix NavigationPageHandler, StepperHandler, TimePickerHandler from decompiled
NavigationPageHandler:
- Added LoadToolbarIcon() method for PNG/SVG toolbar icons
- Added icon loading in MapToolbarItems()
- Fixed OnVirtualViewPushed to set Title and handle null content
- Fixed animation parameters to match decompiled

StepperHandler:
- Added MapIncrement() and MapIsEnabled() methods
- Added dark theme color support in ConnectHandler

TimePickerHandler:
- Added dark theme color support in ConnectHandler

SkiaPage:
- Added Icon property to SkiaToolbarItem class

Also added Svg.Skia package reference.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 12:52:33 -05:00
1cdf66c44b Verify handlers and fix ImageButtonHandler
Verified handlers (method-by-method comparison with decompiled):
- CheckBoxHandler, SwitchHandler, SliderHandler, ProgressBarHandler
- ImageHandler, BoxViewHandler, ScrollViewHandler, EditorHandler

Fixed:
- ImageButtonHandler: Added missing MapBackgroundColor method

Blocked:
- BorderHandler: Needs SkiaBorder.MauiView and Tapped event (View issue)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 12:39:58 -05:00
d3feaa8964 Add 5 verified files from decompiled production code
Changes:
- GtkWebViewHandler.cs - New native WebKit handler
- GtkWebViewProxy.cs - New proxy for WebView positioning
- WebViewHandler.cs - Fixed navigation event handling
- PageHandler.cs - Added MapBackgroundColor
- SkiaView.cs - Made Arrange() virtual

Also adds CLAUDE.md (instructions) and MERGE_TRACKING.md

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 12:20:28 -05:00
f7043ab9c7 Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext

Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method

Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method

Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator

Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods

Build: SUCCESS (0 errors)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
e02af03be0 Add critical instructions and update tracking
- CLAUDE.md: Document that DECOMPILED = production, MAIN = outdated
- MERGE_TRACKING.md: List files incorrectly skipped that need comparison
- Must compare ALL files, not skip because "they exist"

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 10:18:40 -05:00
d0d8e92dad Add new type files from decompiled source
- FlexDirection, FlexWrap, FlexJustify, FlexAlignItems, FlexAlignContent, FlexAlignSelf enums
- FlexBasis struct for flex layout
- ContextMenuItem class for context menus
- ISkiaQueryAttributable interface for shell navigation
- SkiaTextSpan class for formatted text

These types support FlexLayout, context menus, and text formatting.
Other types (event args, enums, etc.) were already defined inline in View files.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 10:07:57 -05:00
1e84c6168a Add WebView support via WebKitGTK
Implements Priority 4 item: WebView via WebKitGTK

New files:
- Interop/WebKitGtk.cs - P/Invoke bindings for WebKitGTK library
- Views/LinuxWebView.cs - WebKitGTK-based WebView platform control
- Handlers/WebViewHandler.Linux.cs - MAUI handler for WebView on Linux
- samples_temp/WebViewDemo/ - Demo app for WebView functionality

Features:
- Full HTML5/CSS3/JavaScript support via WebKitGTK
- Navigation (back/forward/reload)
- URL and HTML source loading
- JavaScript evaluation
- Navigation events (Navigating/Navigated)
- Automatic GTK event processing

Requirements:
- libwebkit2gtk-4.1-0 package on target Linux system

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 10:46:11 -05:00
Admin
10a061777e fix(ci): remove pwsh dependency from release workflow 2025-12-28 10:44:15 -05:00
0dcb76695e Update sign-off: all tests passing, ready for 1.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 10:19:12 -05:00
10222090fd Implement architecture improvements for 1.0 release
Priority 1 - Stability:
- Dirty region invalidation in SkiaRenderingEngine
- Font fallback chain (FontFallbackManager) for emoji/CJK/international text
- Input method polish with Fcitx5 support alongside IBus

Priority 2 - Platform Integration:
- Portal file picker (PortalFilePickerService) with zenity/kdialog fallback
- System theme detection (SystemThemeService) for GNOME/KDE/XFCE/etc
- Notification actions support with D-Bus callbacks

Priority 3 - Performance:
- GPU acceleration (GpuRenderingEngine) with OpenGL, software fallback
- Virtualization manager (VirtualizationManager) for list recycling

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 09:53:40 -05:00
b18d5a11f3 RC1: Full XAML support with BindableProperty, VSM, and data binding
Phase 1 - BindableProperty Foundation:
- SkiaLayoutView: Convert Spacing, Padding, ClipToBounds to BindableProperty
- SkiaStackLayout: Convert Orientation to BindableProperty
- SkiaGrid: Convert RowSpacing, ColumnSpacing to BindableProperty
- SkiaCollectionView: Convert all 12 properties to BindableProperty
- SkiaShell: Convert all 12 properties to BindableProperty

Phase 2 - Visual State Manager:
- Add VSM integration to SkiaImageButton pointer handlers
- Support Normal, PointerOver, Pressed, Disabled states

Phase 3-4 - XAML/Data Binding:
- Type converters for SKColor, SKRect, SKSize, SKPoint
- BindingContext propagation through visual tree
- Full handler registration for all MAUI controls

Documentation:
- README: Add styling/binding examples, update roadmap
- Add RC1-ROADMAP.md with implementation details

Version: 1.0.0-rc.1

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 09:26:21 -05:00
Admin
2719ddf720 feat(ci): add NuGet push to nuget.org on main branch 2025-12-28 06:51:13 -05:00
Admin
7e58513ab3 Remove unsupported artifact upload 2025-12-27 13:05:55 -05:00
Admin
a450daa86f Remove pwsh dependency 2025-12-27 12:22:24 -05:00
Admin
c8840f2e8b Use explicit .NET 9 path in workflows 2025-12-27 12:06:47 -05:00
Admin
f0dbd29b58 Rebuild with fixed PATH 2025-12-27 12:01:33 -05:00
Admin
a4f04f4966 Rebuild with .NET 9 PATH 2025-12-27 11:54:40 -05:00
Admin
a03c600864 Rebuild with Node.js 2025-12-27 11:48:09 -05:00
Admin
0c460c1395 Trigger CI build 2025-12-27 11:44:46 -05:00
Admin
0dd7a2d3fb Add Gitea Actions workflows for CI and NuGet release 2025-12-27 11:34:23 -05:00
afbf8f6782 Fix layout rendering, text wrapping, and scrollbar issues
- LinuxViewRenderer: Remove redundant child rendering that caused "View already has a parent" errors
- SkiaLabel: Consider LineBreakMode.WordWrap for multi-line measurement and rendering
- SkiaScrollView: Update ContentSize in OnDraw with properly constrained measurement
- SkiaShell: Account for padding in MeasureOverride (consistent with ArrangeOverride)
- Version bump to 1.0.0-preview.4

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:20:27 -05:00
02b3da17d4 Update repository URLs to git.marketally.com
Migrated from GitHub to self-hosted Gitea server.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 09:45:26 -05:00
logikonline
299914d077 Add package icon for NuGet
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 14:08:27 -05:00
logikonline
ed09456d57 Move samples to dedicated repository
Samples are now in https://github.com/open-maui/maui-linux-samples

This keeps the framework repo focused and allows samples to:
- Reference NuGet package (real-world usage)
- Be cloned independently
- Have their own release cycle

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 13:41:52 -05:00
logikonline
1d55ac672a Preview 3: Complete control implementation with XAML data binding
Major milestone adding full control functionality:

Controls Enhanced:
- Entry/Editor: Full keyboard input, cursor navigation, selection, clipboard
- CollectionView: Data binding, selection highlighting, scrolling
- CheckBox/Switch/Slider: Interactive state management
- Picker/DatePicker/TimePicker: Dropdown selection with popup overlays
- ProgressBar/ActivityIndicator: Animated progress display
- Button: Press/release visual states
- Border/Frame: Rounded corners, stroke styling
- Label: Text wrapping, alignment, decorations
- Grid/StackLayout: Margin and padding support

Features Added:
- DisplayAlert dialogs with button actions
- NavigationPage with toolbar and back navigation
- Shell with flyout menu navigation
- XAML value converters for data binding
- Margin support in all layout containers
- Popup overlay system for pickers

New Samples:
- TodoApp: Full CRUD task manager with NavigationPage
- ShellDemo: Comprehensive control showcase

Removed:
- ControlGallery (replaced by ShellDemo)
- LinuxDemo (replaced by TodoApp)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 13:26:56 -05:00
logikonline
f945d2a537 Add control gallery sample and roadmap documentation
- Add comprehensive ControlGallery sample app with 12 pages
  demonstrating all 35+ controls
- Add detailed ROADMAP.md with version milestones
- Add README placeholders for VSIX icons and template images
- Sample pages include: Home, Buttons, Labels, Entry, Pickers,
  Sliders, Toggles, Progress, Images, CollectionView, CarouselView,
  SwipeView, RefreshView

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 05:24:35 -05:00
logikonline
1d9338d823 Add full XAML support for .NET MAUI compatibility
New features:
- MauiAppBuilderExtensions.UseOpenMauiLinux() for standard MAUI integration
- New template: openmaui-linux-xaml with full XAML support
- Standard MAUI XAML syntax (ContentPage, VerticalStackLayout, etc.)
- Resource dictionaries (Colors.xaml, Styles.xaml)
- Compiled XAML support via MauiXaml items

Templates now available:
- dotnet new openmaui-linux      (code-based UI)
- dotnet new openmaui-linux-xaml (XAML-based UI)

Usage:
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseOpenMauiLinux();  // Enable Linux with XAML
2025-12-19 05:17:50 -05:00
logikonline
ae5c9ab738 Add Visual Studio extension for Linux platform support
VSIX extension that adds:
- "OpenMaui Linux App" project template in File → New → Project
- Pre-configured launch profiles for Linux debugging
- WSL integration for Windows developers
- x64 and ARM64 build configurations

Launch Profiles included:
- Linux (Local) - Direct execution
- Linux (WSL) - Run via WSL
- Linux (x64 Release) - Release build for x64
- Linux (ARM64 Release) - Release build for ARM64
- Publish Linux x64/ARM64 - Self-contained publishing

Build with: msbuild /p:Configuration=Release
Output: OpenMaui.VisualStudio.vsix
2025-12-19 05:13:16 -05:00
logikonline
d238dde5a4 Add FAQ documentation for Visual Studio integration
- How to add Linux to existing MAUI projects
- Why Linux doesn't appear in VS platform dropdown
- Building for Linux from Windows (CLI, WSL, VM)
- Debugging options (remote, WSL, VM)
- Project structure recommendations
- Build and packaging instructions
- Common issues and solutions
- IDE recommendations
- CI/CD examples
2025-12-19 05:07:57 -05:00
418 changed files with 75587 additions and 9696 deletions

39
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,39 @@
# OpenMaui Linux CI Pipeline
name: CI
on:
push:
branches: [main, final, develop]
pull_request:
branches: [main]
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
build-linux:
name: Build (Linux)
runs-on: linux-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Run tests
run: dotnet test --configuration Release --no-build --verbosity normal
continue-on-error: true
- name: Pack NuGet
run: dotnet pack --configuration Release --no-build -o ./nupkg
- name: Upload NuGet packages
uses: actions/upload-artifact@v3
with:
name: nuget-packages
path: ./nupkg/*.nupkg

View File

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

5
.gitignore vendored
View File

@@ -47,3 +47,8 @@ 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

182
AnimationManager.cs Normal file
View File

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

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

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

@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
namespace Microsoft.Maui.Controls;
/// <summary>
/// Provides attached properties for Entry controls.
/// </summary>
public static class EntryExtensions
{
/// <summary>
/// Attached property for SelectAllOnDoubleClick behavior.
/// When true, double-clicking the entry selects all text instead of just the word.
/// </summary>
public static readonly BindableProperty SelectAllOnDoubleClickProperty =
BindableProperty.CreateAttached(
"SelectAllOnDoubleClick",
typeof(bool),
typeof(EntryExtensions),
false);
/// <summary>
/// Gets the SelectAllOnDoubleClick value for the specified entry.
/// </summary>
public static bool GetSelectAllOnDoubleClick(BindableObject view)
{
return (bool)view.GetValue(SelectAllOnDoubleClickProperty);
}
/// <summary>
/// Sets the SelectAllOnDoubleClick value for the specified entry.
/// </summary>
public static void SetSelectAllOnDoubleClick(BindableObject view, bool value)
{
view.SetValue(SelectAllOnDoubleClickProperty, value);
}
}

View File

@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public static class ColorExtensions
{
public static SKColor ToSKColor(this Color color)
{
return SKColorTypeConverter.ToSKColor(color);
}
public static Color ToMauiColor(this SKColor color)
{
return SKColorTypeConverter.ToMauiColor(color);
}
}

View File

@@ -0,0 +1,237 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using SkiaSharp;
using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Platform.Linux.Converters;
/// <summary>
/// Type converter for converting between MAUI Color and SKColor.
/// Enables XAML styling with Color values that get applied to Skia controls.
/// </summary>
public class SKColorTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Color) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Color) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Color mauiColor)
{
return ToSKColor(mauiColor);
}
if (value is string str)
{
return ParseColor(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKColor skColor)
{
if (destinationType == typeof(string))
{
return $"#{skColor.Alpha:X2}{skColor.Red:X2}{skColor.Green:X2}{skColor.Blue:X2}";
}
if (destinationType == typeof(Color))
{
return ToMauiColor(skColor);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Converts a MAUI Color to an SKColor.
/// </summary>
public static SKColor ToSKColor(Color mauiColor)
{
return new SKColor(
(byte)(mauiColor.Red * 255),
(byte)(mauiColor.Green * 255),
(byte)(mauiColor.Blue * 255),
(byte)(mauiColor.Alpha * 255));
}
/// <summary>
/// Converts an SKColor to a MAUI Color.
/// </summary>
public static Color ToMauiColor(SKColor skColor)
{
return new Color(
skColor.Red / 255f,
skColor.Green / 255f,
skColor.Blue / 255f,
skColor.Alpha / 255f);
}
/// <summary>
/// Parses a color string (hex, named, or rgb format).
/// </summary>
private static SKColor ParseColor(string colorString)
{
if (string.IsNullOrWhiteSpace(colorString))
return SKColors.Black;
colorString = colorString.Trim();
// Try hex format
if (colorString.StartsWith("#"))
{
return SKColor.Parse(colorString);
}
// Try named colors
var namedColor = GetNamedColor(colorString.ToLowerInvariant());
if (namedColor.HasValue)
return namedColor.Value;
// Try rgb/rgba format
if (colorString.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
{
return ParseRgbColor(colorString);
}
// Fallback to SKColor.Parse
if (SKColor.TryParse(colorString, out var parsed))
return parsed;
return SKColors.Black;
}
private static SKColor? GetNamedColor(string name) => name switch
{
"transparent" => SKColors.Transparent,
"black" => SKColors.Black,
"white" => SKColors.White,
"red" => SKColors.Red,
"green" => SKColors.Green,
"blue" => SKColors.Blue,
"yellow" => SKColors.Yellow,
"cyan" => SKColors.Cyan,
"magenta" => SKColors.Magenta,
"gray" or "grey" => SKColors.Gray,
"darkgray" or "darkgrey" => SKColors.DarkGray,
"lightgray" or "lightgrey" => SKColors.LightGray,
"orange" => new SKColor(0xFF, 0xA5, 0x00),
"pink" => new SKColor(0xFF, 0xC0, 0xCB),
"purple" => new SKColor(0x80, 0x00, 0x80),
"brown" => new SKColor(0xA5, 0x2A, 0x2A),
"navy" => new SKColor(0x00, 0x00, 0x80),
"teal" => new SKColor(0x00, 0x80, 0x80),
"olive" => new SKColor(0x80, 0x80, 0x00),
"silver" => new SKColor(0xC0, 0xC0, 0xC0),
"maroon" => new SKColor(0x80, 0x00, 0x00),
"lime" => new SKColor(0x00, 0xFF, 0x00),
"aqua" => new SKColor(0x00, 0xFF, 0xFF),
"fuchsia" => new SKColor(0xFF, 0x00, 0xFF),
"gold" => new SKColor(0xFF, 0xD7, 0x00),
"coral" => new SKColor(0xFF, 0x7F, 0x50),
"salmon" => new SKColor(0xFA, 0x80, 0x72),
"crimson" => new SKColor(0xDC, 0x14, 0x3C),
"indigo" => new SKColor(0x4B, 0x00, 0x82),
"violet" => new SKColor(0xEE, 0x82, 0xEE),
"turquoise" => new SKColor(0x40, 0xE0, 0xD0),
"tan" => new SKColor(0xD2, 0xB4, 0x8C),
"chocolate" => new SKColor(0xD2, 0x69, 0x1E),
"tomato" => new SKColor(0xFF, 0x63, 0x47),
"steelblue" => new SKColor(0x46, 0x82, 0xB4),
"skyblue" => new SKColor(0x87, 0xCE, 0xEB),
"slategray" or "slategrey" => new SKColor(0x70, 0x80, 0x90),
"seagreen" => new SKColor(0x2E, 0x8B, 0x57),
"royalblue" => new SKColor(0x41, 0x69, 0xE1),
"plum" => new SKColor(0xDD, 0xA0, 0xDD),
"peru" => new SKColor(0xCD, 0x85, 0x3F),
"orchid" => new SKColor(0xDA, 0x70, 0xD6),
"orangered" => new SKColor(0xFF, 0x45, 0x00),
"olivedrab" => new SKColor(0x6B, 0x8E, 0x23),
"midnightblue" => new SKColor(0x19, 0x19, 0x70),
"mediumblue" => new SKColor(0x00, 0x00, 0xCD),
"limegreen" => new SKColor(0x32, 0xCD, 0x32),
"hotpink" => new SKColor(0xFF, 0x69, 0xB4),
"honeydew" => new SKColor(0xF0, 0xFF, 0xF0),
"greenyellow" => new SKColor(0xAD, 0xFF, 0x2F),
"forestgreen" => new SKColor(0x22, 0x8B, 0x22),
"firebrick" => new SKColor(0xB2, 0x22, 0x22),
"dodgerblue" => new SKColor(0x1E, 0x90, 0xFF),
"deeppink" => new SKColor(0xFF, 0x14, 0x93),
"deepskyblue" => new SKColor(0x00, 0xBF, 0xFF),
"darkviolet" => new SKColor(0x94, 0x00, 0xD3),
"darkturquoise" => new SKColor(0x00, 0xCE, 0xD1),
"darkslategray" or "darkslategrey" => new SKColor(0x2F, 0x4F, 0x4F),
"darkred" => new SKColor(0x8B, 0x00, 0x00),
"darkorange" => new SKColor(0xFF, 0x8C, 0x00),
"darkolivegreen" => new SKColor(0x55, 0x6B, 0x2F),
"darkmagenta" => new SKColor(0x8B, 0x00, 0x8B),
"darkkhaki" => new SKColor(0xBD, 0xB7, 0x6B),
"darkgreen" => new SKColor(0x00, 0x64, 0x00),
"darkgoldenrod" => new SKColor(0xB8, 0x86, 0x0B),
"darkcyan" => new SKColor(0x00, 0x8B, 0x8B),
"darkblue" => new SKColor(0x00, 0x00, 0x8B),
"cornflowerblue" => new SKColor(0x64, 0x95, 0xED),
"cadetblue" => new SKColor(0x5F, 0x9E, 0xA0),
"blueviolet" => new SKColor(0x8A, 0x2B, 0xE2),
"azure" => new SKColor(0xF0, 0xFF, 0xFF),
"aquamarine" => new SKColor(0x7F, 0xFF, 0xD4),
"aliceblue" => new SKColor(0xF0, 0xF8, 0xFF),
_ => null
};
private static SKColor ParseRgbColor(string colorString)
{
try
{
var isRgba = colorString.StartsWith("rgba", StringComparison.OrdinalIgnoreCase);
var startIndex = colorString.IndexOf('(');
var endIndex = colorString.IndexOf(')');
if (startIndex == -1 || endIndex == -1)
return SKColors.Black;
var values = colorString.Substring(startIndex + 1, endIndex - startIndex - 1)
.Split(',')
.Select(v => v.Trim())
.ToArray();
if (values.Length < 3)
return SKColors.Black;
var r = byte.Parse(values[0]);
var g = byte.Parse(values[1]);
var b = byte.Parse(values[2]);
byte a = 255;
if (isRgba && values.Length >= 4)
{
var alphaValue = float.Parse(values[3], CultureInfo.InvariantCulture);
a = (byte)(alphaValue <= 1 ? alphaValue * 255 : alphaValue);
}
return new SKColor(r, g, b, a);
}
catch
{
return SKColors.Black;
}
}
}

View File

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

View File

@@ -0,0 +1,139 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using SkiaSharp;
using Microsoft.Maui;
namespace Microsoft.Maui.Platform.Linux.Converters;
/// <summary>
/// Type converter for converting between MAUI Thickness and SKRect (for padding/margin).
/// Enables XAML styling with Thickness values that get applied to Skia controls.
/// </summary>
public class SKRectTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Thickness) ||
sourceType == typeof(double) ||
sourceType == typeof(float) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Thickness) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Thickness thickness)
{
return ThicknessToSKRect(thickness);
}
if (value is double d)
{
return new SKRect((float)d, (float)d, (float)d, (float)d);
}
if (value is float f)
{
return new SKRect(f, f, f, f);
}
if (value is string str)
{
return ParseRect(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKRect rect)
{
if (destinationType == typeof(string))
{
return $"{rect.Left},{rect.Top},{rect.Right},{rect.Bottom}";
}
if (destinationType == typeof(Thickness))
{
return SKRectToThickness(rect);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Converts a MAUI Thickness to an SKRect (used as padding storage).
/// </summary>
public static SKRect ThicknessToSKRect(Thickness thickness)
{
return new SKRect(
(float)thickness.Left,
(float)thickness.Top,
(float)thickness.Right,
(float)thickness.Bottom);
}
/// <summary>
/// Converts an SKRect (used as padding storage) to a MAUI Thickness.
/// </summary>
public static Thickness SKRectToThickness(SKRect rect)
{
return new Thickness(rect.Left, rect.Top, rect.Right, rect.Bottom);
}
/// <summary>
/// Parses a string into an SKRect for padding/margin.
/// Supports formats: "uniform", "horizontal,vertical", "left,top,right,bottom"
/// </summary>
private static SKRect ParseRect(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKRect.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1)
{
// Uniform padding
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform))
{
return new SKRect(uniform, uniform, uniform, uniform);
}
}
else if (parts.Length == 2)
{
// Horizontal, Vertical
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var horizontal) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var vertical))
{
return new SKRect(horizontal, vertical, horizontal, vertical);
}
}
else if (parts.Length == 4)
{
// Left, Top, Right, Bottom
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var left) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var top) &&
float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var right) &&
float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bottom))
{
return new SKRect(left, top, right, bottom);
}
}
return SKRect.Empty;
}
}

View File

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

View File

@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Converters;
public static class SKTypeExtensions
{
public static SKRect ToSKRect(this Thickness thickness)
{
return SKRectTypeConverter.ThicknessToSKRect(thickness);
}
public static Thickness ToThickness(this SKRect rect)
{
return SKRectTypeConverter.SKRectToThickness(rect);
}
public static SKSize ToSKSize(this Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
public static Size ToSize(this SKSize size)
{
return new Size(size.Width, size.Height);
}
public static SKPoint ToSKPoint(this Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
public static Point ToPoint(this SKPoint point)
{
return new Point(point.X, point.Y);
}
}

View File

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

View File

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

View File

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

11
DisplayServerType.cs Normal file
View File

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

53
Easing.cs Normal file
View File

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

View File

@@ -1,43 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for ActivityIndicator control.
/// </summary>
public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator, SkiaActivityIndicator>
{
public static IPropertyMapper<IActivityIndicator, ActivityIndicatorHandler> Mapper = new PropertyMapper<IActivityIndicator, ActivityIndicatorHandler>(ViewHandler.ViewMapper)
{
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
[nameof(IActivityIndicator.Color)] = MapColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public ActivityIndicatorHandler() : base(Mapper, CommandMapper) { }
protected override SkiaActivityIndicator CreatePlatformView() => new SkiaActivityIndicator();
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
handler.PlatformView.IsRunning = activityIndicator.IsRunning;
}
public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator.Color != null)
handler.PlatformView.Color = activityIndicator.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
handler.PlatformView.IsEnabled = activityIndicator.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

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

View File

@@ -0,0 +1,137 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform.Linux.Hosting;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for MAUI Application on Linux.
/// Bridges the MAUI Application lifecycle with LinuxApplication.
/// </summary>
public partial class ApplicationHandler : ElementHandler<IApplication, LinuxApplicationContext>
{
public static IPropertyMapper<IApplication, ApplicationHandler> Mapper =
new PropertyMapper<IApplication, ApplicationHandler>(ElementHandler.ElementMapper)
{
};
public static CommandMapper<IApplication, ApplicationHandler> CommandMapper =
new(ElementHandler.ElementCommandMapper)
{
[nameof(IApplication.OpenWindow)] = MapOpenWindow,
[nameof(IApplication.CloseWindow)] = MapCloseWindow,
};
public ApplicationHandler() : base(Mapper, CommandMapper)
{
}
public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override LinuxApplicationContext CreatePlatformElement()
{
return new LinuxApplicationContext();
}
protected override void ConnectHandler(LinuxApplicationContext platformView)
{
base.ConnectHandler(platformView);
platformView.Application = VirtualView;
}
protected override void DisconnectHandler(LinuxApplicationContext platformView)
{
platformView.Application = null;
base.DisconnectHandler(platformView);
}
public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args)
{
if (args is IWindow window)
{
handler.PlatformView?.OpenWindow(window);
}
}
public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args)
{
if (args is IWindow window)
{
handler.PlatformView?.CloseWindow(window);
}
}
}
/// <summary>
/// Platform context for the MAUI Application on Linux.
/// Manages windows and the application lifecycle.
/// </summary>
public class LinuxApplicationContext
{
private readonly List<IWindow> _windows = new();
private IApplication? _application;
/// <summary>
/// Gets or sets the MAUI Application.
/// </summary>
public IApplication? Application
{
get => _application;
set
{
_application = value;
if (_application != null)
{
// Initialize windows from the application
foreach (var window in _application.Windows)
{
if (!_windows.Contains(window))
{
_windows.Add(window);
}
}
}
}
}
/// <summary>
/// Gets the list of open windows.
/// </summary>
public IReadOnlyList<IWindow> Windows => _windows;
/// <summary>
/// Opens a window and creates its handler.
/// </summary>
public void OpenWindow(IWindow window)
{
if (!_windows.Contains(window))
{
_windows.Add(window);
}
}
/// <summary>
/// Closes a window and cleans up its handler.
/// </summary>
public void CloseWindow(IWindow window)
{
_windows.Remove(window);
if (_windows.Count == 0)
{
// Last window closed, stop the application
LinuxApplication.Current?.MainWindow?.Stop();
}
}
/// <summary>
/// Gets the main window of the application.
/// </summary>
public IWindow? MainWindow => _windows.Count > 0 ? _windows[0] : null;
}

View File

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

View File

@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for BoxView on Linux.
/// </summary>
public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
{
public static IPropertyMapper<BoxView, BoxViewHandler> Mapper =
new PropertyMapper<BoxView, BoxViewHandler>(ViewMapper)
{
[nameof(BoxView.Color)] = MapColor,
[nameof(BoxView.CornerRadius)] = MapCornerRadius,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public BoxViewHandler() : base(Mapper)
{
}
protected override SkiaBoxView CreatePlatformView()
{
return new SkiaBoxView();
}
protected override void ConnectHandler(SkiaBoxView platformView)
{
base.ConnectHandler(platformView);
// Map size requests from MAUI BoxView
if (VirtualView is BoxView boxView)
{
if (boxView.WidthRequest >= 0)
platformView.WidthRequest = boxView.WidthRequest;
if (boxView.HeightRequest >= 0)
platformView.HeightRequest = boxView.HeightRequest;
}
}
public static void MapColor(BoxViewHandler handler, BoxView boxView)
{
if (boxView.Color != null)
{
handler.PlatformView.Color = boxView.Color;
}
}
public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView)
{
handler.PlatformView.CornerRadius = boxView.CornerRadius;
}
public static void MapBackground(BoxViewHandler handler, BoxView boxView)
{
if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color;
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView)
{
if (boxView.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = boxView.BackgroundColor;
handler.PlatformView.Invalidate();
}
}
}

View File

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

View File

@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -20,6 +22,7 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
[nameof(IView.Background)] = MapBackground,
[nameof(IPadding.Padding)] = MapPadding,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -47,6 +50,32 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
platformView.Clicked += OnClicked;
platformView.Pressed += OnPressed;
platformView.Released += OnReleased;
// Manually map all properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView != null)
{
MapStrokeColor(this, VirtualView);
MapStrokeThickness(this, VirtualView);
MapCornerRadius(this, VirtualView);
MapBackground(this, VirtualView);
MapPadding(this, VirtualView);
MapIsEnabled(this, VirtualView);
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
DiagnosticLog.Debug("ButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
else
{
DiagnosticLog.Debug("ButtonHandler", $"VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
}
}
}
protected override void DisconnectHandler(SkiaButton platformView)
@@ -67,13 +96,13 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
var strokeColor = button.StrokeColor;
if (strokeColor is not null)
handler.PlatformView.BorderColor = strokeColor.ToSKColor();
handler.PlatformView.BorderColor = strokeColor;
}
public static void MapStrokeThickness(ButtonHandler handler, IButton button)
{
if (handler.PlatformView is null) return;
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
handler.PlatformView.BorderWidth = button.StrokeThickness;
}
public static void MapCornerRadius(ButtonHandler handler, IButton button)
@@ -88,7 +117,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
// Set BackgroundColor (MAUI Color type)
handler.PlatformView.BackgroundColor = solidPaint.Color;
}
}
@@ -97,11 +127,18 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (handler.PlatformView is null) return;
var padding = button.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.Padding = new Thickness(
padding.Left,
padding.Top,
padding.Right,
padding.Bottom);
}
public static void MapIsEnabled(ButtonHandler handler, IButton button)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}
}
@@ -124,6 +161,33 @@ public partial class TextButtonHandler : ButtonHandler
{
}
protected override void ConnectHandler(SkiaButton platformView)
{
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler START");
base.ConnectHandler(platformView);
// Manually map text properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView is ITextButton textButton)
{
MapText(this, textButton);
MapTextColor(this, textButton);
MapFont(this, textButton);
MapCharacterSpacing(this, textButton);
}
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
DiagnosticLog.Debug("TextButtonHandler", $"MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
DiagnosticLog.Debug("TextButtonHandler", "ConnectHandler DONE");
}
public static void MapText(TextButtonHandler handler, ITextButton button)
{
if (handler.PlatformView is null) return;
@@ -135,7 +199,7 @@ public partial class TextButtonHandler : ButtonHandler
if (handler.PlatformView is null) return;
if (button.TextColor is not null)
handler.PlatformView.TextColor = button.TextColor.ToSKColor();
handler.PlatformView.TextColor = button.TextColor;
}
public static void MapFont(TextButtonHandler handler, ITextButton button)
@@ -144,18 +208,23 @@ public partial class TextButtonHandler : ButtonHandler
var font = button.Font;
if (font.Size > 0)
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.FontSize = font.Size;
if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family;
handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold;
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
// Convert Font weight/slant to FontAttributes
FontAttributes attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
}
public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button)
{
if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = (float)button.CharacterSpacing;
handler.PlatformView.CharacterSpacing = button.CharacterSpacing;
}
}

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

View File

@@ -1,93 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for CheckBox control.
/// </summary>
public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
{
/// <summary>
/// Maps the property mapper for the handler.
/// </summary>
public static IPropertyMapper<ICheckBox, CheckBoxHandler> Mapper = new PropertyMapper<ICheckBox, CheckBoxHandler>(ViewHandler.ViewMapper)
{
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
public CheckBoxHandler() : base(Mapper, CommandMapper)
{
}
public CheckBoxHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaCheckBox CreatePlatformView()
{
return new SkiaCheckBox();
}
protected override void ConnectHandler(SkiaCheckBox platformView)
{
base.ConnectHandler(platformView);
platformView.CheckedChanged += OnCheckedChanged;
}
protected override void DisconnectHandler(SkiaCheckBox platformView)
{
platformView.CheckedChanged -= OnCheckedChanged;
base.DisconnectHandler(platformView);
}
private void OnCheckedChanged(object? sender, CheckedChangedEventArgs e)
{
if (VirtualView != null && VirtualView.IsChecked != e.IsChecked)
{
VirtualView.IsChecked = e.IsChecked;
}
}
public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView.IsChecked != checkBox.IsChecked)
{
handler.PlatformView.IsChecked = checkBox.IsChecked;
}
}
public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox)
{
var foreground = checkBox.Foreground;
if (foreground is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BoxColor = solidBrush.Color.ToSKColor();
}
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
{
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -18,6 +18,9 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
};
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -70,7 +73,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.CheckColor = solidPaint.Color.ToSKColor();
handler.PlatformView.CheckColor = solidPaint.Color;
}
}
@@ -80,7 +83,41 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.Color = solidPaint.Color;
}
}
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
}
public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.VerticalOptions = checkBox.VerticalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Fill
};
}
public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalOptions = checkBox.HorizontalLayoutAlignment switch
{
Primitives.LayoutAlignment.Start => LayoutOptions.Start,
Primitives.LayoutAlignment.Center => LayoutOptions.Center,
Primitives.LayoutAlignment.End => LayoutOptions.End,
Primitives.LayoutAlignment.Fill => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}

View File

@@ -5,6 +5,8 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -15,6 +17,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary>
public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCollectionView>
{
private bool _isUpdatingSelection;
public static IPropertyMapper<CollectionView, CollectionViewHandler> Mapper =
new PropertyMapper<CollectionView, CollectionViewHandler>(ViewHandler.ViewMapper)
{
@@ -36,6 +40,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout,
[nameof(IView.Background)] = MapBackground,
[nameof(CollectionView.BackgroundColor)] = MapBackgroundColor,
};
public static CommandMapper<CollectionView, CollectionViewHandler> CommandMapper =
@@ -76,21 +81,34 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e)
{
if (VirtualView is null) return;
if (VirtualView is null || _isUpdatingSelection) return;
// Update virtual view selection
if (VirtualView.SelectionMode == SelectionMode.Single)
try
{
VirtualView.SelectedItem = e.CurrentSelection.FirstOrDefault();
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
// Clear and update selected items
VirtualView.SelectedItems.Clear();
foreach (var item in e.CurrentSelection)
_isUpdatingSelection = true;
// Update virtual view selection
if (VirtualView.SelectionMode == SelectionMode.Single)
{
VirtualView.SelectedItems.Add(item);
var newItem = e.CurrentSelection.FirstOrDefault();
if (!Equals(VirtualView.SelectedItem, newItem))
{
VirtualView.SelectedItem = newItem;
}
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
// Clear and update selected items
VirtualView.SelectedItems.Clear();
foreach (var item in e.CurrentSelection)
{
VirtualView.SelectedItems.Add(item);
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
@@ -107,7 +125,49 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e)
{
// Item tap is handled through selection
if (VirtualView is null || _isUpdatingSelection) return;
try
{
_isUpdatingSelection = true;
DiagnosticLog.Debug("CollectionViewHandler", $"OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}");
// Try to get the item view and process gestures
var skiaView = PlatformView?.GetItemView(e.Index);
DiagnosticLog.Debug("CollectionViewHandler", $"GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? "null"}, MauiView={skiaView?.MauiView?.GetType().Name ?? "null"}");
if (skiaView?.MauiView != null)
{
DiagnosticLog.Debug("CollectionViewHandler", $"Found MauiView: {skiaView.MauiView.GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}");
if (GestureManager.ProcessTap(skiaView.MauiView, 0, 0))
{
DiagnosticLog.Debug("CollectionViewHandler", "Gesture processed successfully");
return;
}
}
// Handle selection if gesture wasn't processed
if (VirtualView.SelectionMode == SelectionMode.Single)
{
VirtualView.SelectedItem = e.Item;
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
if (VirtualView.SelectedItems.Contains(e.Item))
{
VirtualView.SelectedItems.Remove(e.Item);
}
else
{
VirtualView.SelectedItems.Add(e.Item);
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView)
@@ -118,7 +178,68 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView)
{
handler.PlatformView?.Invalidate();
if (handler.PlatformView is null || handler.MauiContext is null) return;
var template = collectionView.ItemTemplate;
if (template != null)
{
// Set up a renderer that creates views from the DataTemplate
handler.PlatformView.ItemViewCreator = (item) =>
{
try
{
// Create view from template
var content = template.CreateContent();
if (content is View view)
{
// Set binding context FIRST so bindings evaluate
view.BindingContext = item;
// Force binding evaluation by accessing the visual tree
// This ensures child bindings are evaluated before handler creation
PropagateBindingContext(view, item);
// Create handler for the view
if (view.Handler == null && handler.MauiContext != null)
{
view.Handler = view.ToViewHandler(handler.MauiContext);
}
if (view.Handler?.PlatformView is SkiaView skiaView)
{
// Set MauiView so gestures can be processed
skiaView.MauiView = view;
DiagnosticLog.Debug("CollectionViewHandler", $"ItemViewCreator: Set MauiView={view.GetType().Name} on {skiaView.GetType().Name}, GestureRecognizers={view.GestureRecognizers?.Count ?? 0}");
return skiaView;
}
}
else if (content is ViewCell cell)
{
cell.BindingContext = item;
var cellView = cell.View;
if (cellView != null)
{
if (cellView.Handler == null && handler.MauiContext != null)
{
cellView.Handler = cellView.ToViewHandler(handler.MauiContext);
}
if (cellView.Handler?.PlatformView is SkiaView skiaView)
{
return skiaView;
}
}
}
}
catch
{
// Ignore template creation errors
}
return null;
};
}
handler.PlatformView.Invalidate();
}
public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView)
@@ -146,19 +267,40 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
try
{
handler._isUpdatingSelection = true;
if (!Equals(handler.PlatformView.SelectedItem, collectionView.SelectedItem))
{
handler.PlatformView.SelectedItem = collectionView.SelectedItem;
}
}
finally
{
handler._isUpdatingSelection = false;
}
}
public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
if (handler.PlatformView is null || handler._isUpdatingSelection) return;
// Sync selected items
var selectedItems = collectionView.SelectedItems;
if (selectedItems != null && selectedItems.Count > 0)
try
{
handler.PlatformView.SelectedItem = selectedItems.First();
handler._isUpdatingSelection = true;
// Sync selected items
var selectedItems = collectionView.SelectedItems;
if (selectedItems != null && selectedItems.Count > 0)
{
handler.PlatformView.SelectedItem = selectedItems.First();
}
}
finally
{
handler._isUpdatingSelection = false;
}
}
@@ -214,9 +356,23 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
{
if (handler.PlatformView is null) return;
// Don't override if BackgroundColor is explicitly set
if (collectionView.BackgroundColor is not null)
return;
if (collectionView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
public static void MapBackgroundColor(CollectionViewHandler handler, CollectionView collectionView)
{
if (handler.PlatformView is null) return;
if (collectionView.BackgroundColor is not null)
{
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor;
}
}
@@ -234,4 +390,32 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated);
}
}
/// <summary>
/// Recursively propagates binding context to all child views to force binding evaluation.
/// </summary>
private static void PropagateBindingContext(View view, object? bindingContext)
{
view.BindingContext = bindingContext;
// Propagate to children
if (view is Layout layout)
{
foreach (var child in layout.Children)
{
if (child is View childView)
{
PropagateBindingContext(childView, bindingContext);
}
}
}
else if (view is ContentView contentView && contentView.Content != null)
{
PropagateBindingContext(contentView.Content, bindingContext);
}
else if (view is Border border && border.Content is View borderContent)
{
PropagateBindingContext(borderContent, bindingContext);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

122
Handlers/FrameHandler.cs Normal file
View File

@@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for Frame on Linux using SkiaFrame.
/// </summary>
public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
{
public static IPropertyMapper<Frame, FrameHandler> Mapper =
new PropertyMapper<Frame, FrameHandler>(ViewMapper)
{
[nameof(Frame.BorderColor)] = MapBorderColor,
[nameof(Frame.CornerRadius)] = MapCornerRadius,
[nameof(Frame.HasShadow)] = MapHasShadow,
[nameof(Frame.BackgroundColor)] = MapBackgroundColor,
[nameof(Frame.Padding)] = MapPadding,
[nameof(Frame.Content)] = MapContent,
};
public FrameHandler() : base(Mapper)
{
}
public FrameHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper)
{
}
protected override SkiaFrame CreatePlatformView()
{
return new SkiaFrame();
}
protected override void ConnectHandler(SkiaFrame platformView)
{
base.ConnectHandler(platformView);
if (VirtualView is View view)
{
platformView.MauiView = view;
}
platformView.Tapped += OnPlatformViewTapped;
}
protected override void DisconnectHandler(SkiaFrame platformView)
{
platformView.Tapped -= OnPlatformViewTapped;
platformView.MauiView = null;
base.DisconnectHandler(platformView);
}
private void OnPlatformViewTapped(object? sender, EventArgs e)
{
if (VirtualView is View view)
{
GestureManager.ProcessTap(view, 0.0, 0.0);
}
}
public static void MapBorderColor(FrameHandler handler, Frame frame)
{
if (frame.BorderColor != null)
{
handler.PlatformView.Stroke = frame.BorderColor;
}
}
public static void MapCornerRadius(FrameHandler handler, Frame frame)
{
handler.PlatformView.CornerRadius = frame.CornerRadius;
}
public static void MapHasShadow(FrameHandler handler, Frame frame)
{
handler.PlatformView.HasShadow = frame.HasShadow;
}
public static void MapBackgroundColor(FrameHandler handler, Frame frame)
{
if (frame.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = frame.BackgroundColor;
}
}
public static void MapPadding(FrameHandler handler, Frame frame)
{
handler.PlatformView.SetPadding(
(float)frame.Padding.Left,
(float)frame.Padding.Top,
(float)frame.Padding.Right,
(float)frame.Padding.Bottom);
}
public static void MapContent(FrameHandler handler, Frame frame)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
handler.PlatformView.ClearChildren();
var content = frame.Content;
if (content != null)
{
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
handler.PlatformView.AddChild(skiaContent);
}
}
}
}

856
Handlers/GestureManager.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
@@ -17,7 +19,9 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
public static IPropertyMapper<ILayout, LayoutHandler> Mapper = new PropertyMapper<ILayout, LayoutHandler>(ViewHandler.ViewMapper)
{
[nameof(ILayout.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
[nameof(IPadding.Padding)] = MapPadding,
};
/// <summary>
@@ -53,22 +57,84 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
base.ConnectHandler(platformView);
// Explicitly map BackgroundColor since it may be set before handler creation
// (e.g., in ItemTemplates for CollectionView)
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor;
platformView.Invalidate();
}
// Add existing children (important for template-created views)
if (VirtualView is ILayout layout && MauiContext != null)
{
for (int i = 0; i < layout.Count; i++)
{
var child = layout[i];
if (child == null) continue;
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToViewHandler(MauiContext);
}
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
platformView.AddChild(skiaChild);
}
}
}
}
public static void MapBackground(LayoutHandler handler, ILayout layout)
{
// Don't override if BackgroundColor is explicitly set
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
return;
var background = layout.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
handler.PlatformView.Invalidate();
}
public static void MapBackgroundColor(LayoutHandler handler, ILayout layout)
{
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
handler.PlatformView.Invalidate();
}
}
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
{
handler.PlatformView.ClipToBounds = layout.ClipsToBounds;
handler.PlatformView.Invalidate();
}
public static void MapPadding(LayoutHandler handler, ILayout layout)
{
if (layout is IPadding paddable)
{
var padding = paddable.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
handler.PlatformView.InvalidateMeasure();
handler.PlatformView.Invalidate();
}
}
public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg)
{
if (arg is LayoutHandlerUpdate update)
@@ -194,9 +260,16 @@ public partial class GridHandler : LayoutHandler
{
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
};
public GridHandler() : base(Mapper)
public static new CommandMapper<IGridLayout, GridHandler> GridCommandMapper = new(LayoutHandler.CommandMapper)
{
["Add"] = MapGridAdd,
};
public GridHandler() : base(Mapper, GridCommandMapper)
{
}
@@ -205,6 +278,52 @@ public partial class GridHandler : LayoutHandler
return new SkiaGrid();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}");
base.ConnectHandler(platformView);
// Map definitions on connect
if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null)
{
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}");
UpdateRowDefinitions(grid, gridLayout);
UpdateColumnDefinitions(grid, gridLayout);
// Add existing children (important for template-created views)
for (int i = 0; i < gridLayout.Count; i++)
{
var child = gridLayout[i];
if (child == null) continue;
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}");
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToViewHandler(MauiContext);
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
}
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (child is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Adding child[{i}] at row={row}, col={column}");
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
}
}
DiagnosticLog.Debug("GridHandler", $"ConnectHandler Grid now has {grid.Children.Count} SkiaView children");
}
}
public static void MapColumnSpacing(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
@@ -222,6 +341,79 @@ public partial class GridHandler : LayoutHandler
grid.Invalidate();
}
}
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
{
UpdateRowDefinitions(grid, layout);
grid.InvalidateMeasure();
grid.Invalidate();
}
}
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
{
UpdateColumnDefinitions(grid, layout);
grid.InvalidateMeasure();
grid.Invalidate();
}
}
private static void UpdateRowDefinitions(SkiaGrid grid, IGridLayout layout)
{
grid.RowDefinitions.Clear();
foreach (var rowDef in layout.RowDefinitions)
{
var height = rowDef.Height;
if (height.IsAbsolute)
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Absolute));
else if (height.IsAuto)
grid.RowDefinitions.Add(GridLength.Auto);
else // Star
grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Star));
}
}
private static void UpdateColumnDefinitions(SkiaGrid grid, IGridLayout layout)
{
grid.ColumnDefinitions.Clear();
foreach (var colDef in layout.ColumnDefinitions)
{
var width = colDef.Width;
if (width.IsAbsolute)
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Absolute));
else if (width.IsAuto)
grid.ColumnDefinitions.Add(GridLength.Auto);
else // Star
grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Star));
}
}
public static void MapGridAdd(GridHandler handler, ILayout layout, object? arg)
{
if (arg is LayoutHandlerUpdate update && handler.PlatformView is SkiaGrid grid)
{
var childHandler = update.View.Handler;
if (childHandler?.PlatformView is SkiaView skiaView)
{
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (update.View is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
grid.AddChild(skiaView, row, column, rowSpan, columnSpan);
}
}
}
}
/// <summary>

View File

@@ -3,6 +3,8 @@
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -17,6 +19,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
{
[nameof(ILayout.ClipsToBounds)] = MapClipsToBounds,
[nameof(IView.Background)] = MapBackground,
[nameof(IPadding.Padding)] = MapPadding,
};
public static CommandMapper<ILayout, LayoutHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -42,6 +45,38 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
base.ConnectHandler(platformView);
// Create handlers for all children and add them to the platform view
if (VirtualView == null || MauiContext == null) return;
// Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor;
}
for (int i = 0; i < VirtualView.Count; i++)
{
var child = VirtualView[i];
if (child == null) continue;
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToViewHandler(MauiContext);
}
// Add child's platform view to our layout
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
platformView.AddChild(skiaChild);
}
}
}
public static void MapClipsToBounds(LayoutHandler handler, ILayout layout)
{
if (handler.PlatformView == null) return;
@@ -54,7 +89,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidPaint.Color;
}
}
@@ -102,6 +137,18 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Force re-layout
handler.PlatformView?.InvalidateMeasure();
}
public static void MapPadding(LayoutHandler handler, ILayout layout)
{
if (handler.PlatformView == null) return;
if (layout is IPadding paddable)
{
handler.PlatformView.Padding = paddable.Padding;
handler.PlatformView.InvalidateMeasure();
handler.PlatformView.Invalidate();
}
}
}
/// <summary>
@@ -138,6 +185,29 @@ public partial class StackLayoutHandler : LayoutHandler
return new SkiaStackLayout();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
// Set orientation first
if (platformView is SkiaStackLayout stackLayout && VirtualView is IStackLayout stackView)
{
// Determine orientation based on view type
if (VirtualView is Microsoft.Maui.Controls.HorizontalStackLayout)
{
stackLayout.Orientation = StackOrientation.Horizontal;
}
else if (VirtualView is Microsoft.Maui.Controls.VerticalStackLayout ||
VirtualView is Microsoft.Maui.Controls.StackLayout)
{
stackLayout.Orientation = StackOrientation.Vertical;
}
stackLayout.Spacing = (float)stackView.Spacing;
}
// Let base handle children
base.ConnectHandler(platformView);
}
public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout)
{
if (handler.PlatformView is SkiaStackLayout stackLayout)
@@ -156,6 +226,8 @@ public partial class GridHandler : LayoutHandler
{
[nameof(IGridLayout.RowSpacing)] = MapRowSpacing,
[nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing,
[nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions,
[nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions,
};
public GridHandler() : base(Mapper)
@@ -167,6 +239,75 @@ public partial class GridHandler : LayoutHandler
return new SkiaGrid();
}
protected override void ConnectHandler(SkiaLayoutView platformView)
{
try
{
// Don't call base - we handle children specially for Grid
if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return;
DiagnosticLog.Debug("GridHandler", $"ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols");
// Explicitly map BackgroundColor since it may be set before handler creation
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor;
}
// Explicitly map Padding since it may be set before handler creation
if (VirtualView is IPadding paddable)
{
var padding = paddable.Padding;
platformView.Padding = padding;
DiagnosticLog.Debug("GridHandler", $"Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
}
// Map row/column definitions first
MapRowDefinitions(this, gridLayout);
MapColumnDefinitions(this, gridLayout);
// Add each child with its row/column position
for (int i = 0; i < gridLayout.Count; i++)
{
var child = gridLayout[i];
if (child == null) continue;
DiagnosticLog.Debug("GridHandler", $"Processing child {i}: {child.GetType().Name}");
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToViewHandler(MauiContext);
}
// Get grid position from attached properties
int row = 0, column = 0, rowSpan = 1, columnSpan = 1;
if (child is Microsoft.Maui.Controls.View mauiView)
{
row = Microsoft.Maui.Controls.Grid.GetRow(mauiView);
column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView);
rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView);
columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView);
}
DiagnosticLog.Debug("GridHandler", $"Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}");
// Add child's platform view to our grid
if (child.Handler?.PlatformView is SkiaView skiaChild)
{
grid.AddChild(skiaChild, row, column, rowSpan, columnSpan);
DiagnosticLog.Debug("GridHandler", $"Added child {i} to grid");
}
}
DiagnosticLog.Debug("GridHandler", "ConnectHandler complete");
}
catch (Exception ex)
{
DiagnosticLog.Error("GridHandler", $"EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}", ex);
throw;
}
}
public static void MapRowSpacing(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is SkiaGrid grid)
@@ -182,4 +323,38 @@ public partial class GridHandler : LayoutHandler
grid.ColumnSpacing = (float)layout.ColumnSpacing;
}
}
public static void MapRowDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is not SkiaGrid grid) return;
grid.RowDefinitions.Clear();
foreach (var rowDef in layout.RowDefinitions)
{
var height = rowDef.Height;
if (height.IsAbsolute)
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
else if (height.IsAuto)
grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
else // Star
grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Star));
}
}
public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout)
{
if (handler.PlatformView is not SkiaGrid grid) return;
grid.ColumnDefinitions.Clear();
foreach (var colDef in layout.ColumnDefinitions)
{
var width = colDef.Width;
if (width.IsAbsolute)
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Absolute));
else if (width.IsAuto)
grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto);
else // Star
grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Star));
}
}
}

376
Handlers/MenuBarHandler.cs Normal file
View File

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

View File

@@ -1,11 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
using Svg.Skia;
using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -50,10 +55,15 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
platformView.Popped += OnPopped;
platformView.PoppedToRoot += OnPoppedToRoot;
// Set initial root page if exists
if (VirtualView.CurrentPage != null)
// Subscribe to navigation events from virtual view
if (VirtualView != null)
{
SetupInitialPage();
VirtualView.Pushed += OnVirtualViewPushed;
VirtualView.Popped += OnVirtualViewPopped;
VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot;
// Set up initial navigation stack
SetupNavigationStack();
}
}
@@ -62,16 +72,270 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
platformView.Pushed -= OnPushed;
platformView.Popped -= OnPopped;
platformView.PoppedToRoot -= OnPoppedToRoot;
if (VirtualView != null)
{
VirtualView.Pushed -= OnVirtualViewPushed;
VirtualView.Popped -= OnVirtualViewPopped;
VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot;
}
base.DisconnectHandler(platformView);
}
private void SetupInitialPage()
private void SetupNavigationStack()
{
var currentPage = VirtualView.CurrentPage;
if (currentPage?.Handler?.PlatformView is SkiaPage skiaPage)
if (VirtualView == null || PlatformView == null || MauiContext == null) return;
// Get all pages in the navigation stack
var pages = VirtualView.Navigation.NavigationStack.ToList();
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up {pages.Count} pages");
// If no pages in stack, check CurrentPage
if (pages.Count == 0 && VirtualView.CurrentPage != null)
{
PlatformView.SetRootPage(skiaPage);
DiagnosticLog.Debug("NavigationPageHandler", $"No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}");
pages.Add(VirtualView.CurrentPage);
}
foreach (var page in pages)
{
// Ensure the page has a handler
if (page.Handler == null)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for: {page.Title}");
page.Handler = page.ToViewHandler(MauiContext);
}
DiagnosticLog.Debug("NavigationPageHandler", $"Page handler type: {page.Handler?.GetType().Name}");
DiagnosticLog.Debug("NavigationPageHandler", $"Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}");
if (page.Handler?.PlatformView is SkiaPage skiaPage)
{
// Set navigation bar properties
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = page.Title ?? "";
DiagnosticLog.Debug("NavigationPageHandler", $"SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}");
// If content is null, try to get it from ContentPage
if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
skiaPage.Content = skiaContent;
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}");
}
}
// Map toolbar items
MapToolbarItems(skiaPage, page);
if (PlatformView.StackDepth == 0)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Setting root page: {page.Title}");
PlatformView.SetRootPage(skiaPage);
}
else
{
DiagnosticLog.Debug("NavigationPageHandler", $"Pushing page: {page.Title}");
PlatformView.Push(skiaPage, false);
}
}
else
{
DiagnosticLog.Warn("NavigationPageHandler", $"Failed to get SkiaPage for: {page.Title}");
}
}
}
private readonly Dictionary<Page, (SkiaPage, INotifyCollectionChanged)> _toolbarSubscriptions = new();
private void MapToolbarItems(SkiaPage skiaPage, Page page)
{
if (skiaPage is SkiaContentPage contentPage)
{
DiagnosticLog.Debug("NavigationPageHandler", $"MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}");
contentPage.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Adding toolbar item: '{item.Text}', IconImageSource={item.IconImageSource}, Order={item.Order}");
// Default and Primary should both be treated as Primary (shown in toolbar)
// Only Secondary goes to overflow menu
var order = item.Order == ToolbarItemOrder.Secondary
? SkiaToolbarItemOrder.Secondary
: SkiaToolbarItemOrder.Primary;
// Create a command that invokes the Clicked event
var toolbarItem = item; // Capture for closure
var clickCommand = new RelayCommand(() =>
{
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItem '{toolbarItem.Text}' clicked, invoking...");
// Use IMenuItemController to send the click
if (toolbarItem is IMenuItemController menuController)
{
menuController.Activate();
}
else
{
// Fallback: invoke Command if set
toolbarItem.Command?.Execute(toolbarItem.CommandParameter);
}
});
// Load icon if specified
SKBitmap? icon = null;
if (item.IconImageSource is FileImageSource fileSource && !string.IsNullOrEmpty(fileSource.File))
{
icon = LoadToolbarIcon(fileSource.File);
}
contentPage.ToolbarItems.Add(new SkiaToolbarItem
{
Text = item.Text ?? "",
Icon = icon,
Order = order,
Command = clickCommand
});
}
// Subscribe to ToolbarItems changes if not already subscribed
if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page))
{
DiagnosticLog.Debug("NavigationPageHandler", $"Subscribing to ToolbarItems changes for '{page.Title}'");
notifyCollection.CollectionChanged += (s, e) =>
{
DiagnosticLog.Debug("NavigationPageHandler", $"ToolbarItems changed for '{page.Title}', action={e.Action}");
MapToolbarItems(skiaPage, page);
skiaPage.Invalidate();
};
_toolbarSubscriptions[page] = (skiaPage, notifyCollection);
}
}
}
private SKBitmap? LoadToolbarIcon(string fileName)
{
try
{
string baseDirectory = AppContext.BaseDirectory;
string pngPath = Path.Combine(baseDirectory, fileName);
string svgPath = Path.Combine(baseDirectory, Path.ChangeExtension(fileName, ".svg"));
DiagnosticLog.Debug("NavigationPageHandler", $"LoadToolbarIcon: Looking for {fileName}");
DiagnosticLog.Debug("NavigationPageHandler", $" Trying PNG: {pngPath} (exists: {File.Exists(pngPath)})");
DiagnosticLog.Debug("NavigationPageHandler", $" Trying SVG: {svgPath} (exists: {File.Exists(svgPath)})");
// Try SVG first
if (File.Exists(svgPath))
{
using var svg = new SKSvg();
svg.Load(svgPath);
if (svg.Picture != null)
{
var cullRect = svg.Picture.CullRect;
float scale = 24f / Math.Max(cullRect.Width, cullRect.Height);
var bitmap = new SKBitmap(24, 24, false);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
canvas.Scale(scale);
canvas.DrawPicture(svg.Picture, null);
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded SVG icon: {svgPath}");
return bitmap;
}
}
// Try PNG
if (File.Exists(pngPath))
{
using var stream = File.OpenRead(pngPath);
var result = SKBitmap.Decode(stream);
DiagnosticLog.Debug("NavigationPageHandler", $"Loaded PNG icon: {pngPath}");
return result;
}
DiagnosticLog.Warn("NavigationPageHandler", $"Icon not found: {fileName}");
return null;
}
catch (Exception ex)
{
DiagnosticLog.Error("NavigationPageHandler", $"Error loading icon {fileName}: {ex.Message}", ex);
return null;
}
}
private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
try
{
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Pushed: {e.Page?.Title}");
if (e.Page == null || PlatformView == null || MauiContext == null) return;
// Ensure the page has a handler
if (e.Page.Handler == null)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Creating handler for page: {e.Page.GetType().Name}");
e.Page.Handler = e.Page.ToViewHandler(MauiContext);
DiagnosticLog.Debug("NavigationPageHandler", $"Handler created: {e.Page.Handler?.GetType().Name}");
}
if (e.Page.Handler?.PlatformView is SkiaPage skiaPage)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}");
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = e.Page.Title ?? "";
// Handle content if null
if (skiaPage.Content == null && e.Page is ContentPage contentPage && contentPage.Content != null)
{
DiagnosticLog.Debug("NavigationPageHandler", $"Content is null, creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
skiaPage.Content = skiaContent;
DiagnosticLog.Debug("NavigationPageHandler", $"Set content to: {skiaContent.GetType().Name}");
}
}
DiagnosticLog.Debug("NavigationPageHandler", "Mapping toolbar items");
MapToolbarItems(skiaPage, e.Page);
DiagnosticLog.Debug("NavigationPageHandler", "Pushing page to platform");
PlatformView.Push(skiaPage, false);
DiagnosticLog.Debug("NavigationPageHandler", $"Push complete, thread={Environment.CurrentManagedThreadId}");
}
DiagnosticLog.Debug("NavigationPageHandler", "OnVirtualViewPushed returning");
}
catch (Exception ex)
{
DiagnosticLog.Error("NavigationPageHandler", $"EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}", ex);
throw;
}
}
private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
DiagnosticLog.Debug("NavigationPageHandler", $"VirtualView Popped: {e.Page?.Title}");
// Pop on the platform side to sync with MAUI navigation
PlatformView?.Pop();
}
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
DiagnosticLog.Debug("NavigationPageHandler", "VirtualView PoppedToRoot");
PlatformView?.PopToRoot();
}
private void OnPushed(object? sender, NavigationEventArgs e)
@@ -81,7 +345,12 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
private void OnPopped(object? sender, NavigationEventArgs e)
{
// Sync back to virtual view if needed
// Sync back to virtual view - pop from MAUI navigation stack
if (VirtualView?.Navigation.NavigationStack.Count > 1)
{
// Don't trigger another pop on platform side
VirtualView.Navigation.RemovePage(VirtualView.Navigation.NavigationStack.Last());
}
}
private void OnPoppedToRoot(object? sender, NavigationEventArgs e)
@@ -95,7 +364,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarBackgroundColor is not null)
{
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor();
handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor;
}
}
@@ -105,7 +374,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarBackground is SolidColorBrush solidBrush)
{
handler.PlatformView.BarBackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BarBackgroundColor = solidBrush.Color;
}
}
@@ -115,7 +384,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.BarTextColor is not null)
{
handler.PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor();
handler.PlatformView.BarTextColor = navigationPage.BarTextColor;
}
}
@@ -125,20 +394,35 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
public static void MapRequestNavigation(NavigationPageHandler handler, NavigationPage navigationPage, object? args)
{
if (handler.PlatformView is null || args is not NavigationRequest request)
if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request)
return;
DiagnosticLog.Debug("NavigationPageHandler", $"MapRequestNavigation: {request.NavigationStack.Count} pages");
// Handle navigation request
foreach (var page in request.NavigationStack)
foreach (var view in request.NavigationStack)
{
if (view is not Page page) continue;
// Ensure handler exists
if (page.Handler == null)
{
page.Handler = page.ToViewHandler(handler.MauiContext);
}
if (page.Handler?.PlatformView is SkiaPage skiaPage)
{
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = handler.PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = handler.PlatformView.BarTextColor;
handler.MapToolbarItems(skiaPage, page);
if (handler.PlatformView.StackDepth == 0)
{
handler.PlatformView.SetRootPage(skiaPage);
@@ -151,3 +435,26 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
}
}
}
/// <summary>
/// Simple relay command for invoking actions.
/// </summary>
internal class RelayCommand : System.Windows.Input.ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for ProgressBar control.
/// </summary>
public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar>
{
public static IPropertyMapper<IProgress, ProgressBarHandler> Mapper = new PropertyMapper<IProgress, ProgressBarHandler>(ViewHandler.ViewMapper)
{
[nameof(IProgress.Progress)] = MapProgress,
[nameof(IProgress.ProgressColor)] = MapProgressColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public ProgressBarHandler() : base(Mapper, CommandMapper) { }
protected override SkiaProgressBar CreatePlatformView() => new SkiaProgressBar();
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
{
handler.PlatformView.Progress = progress.Progress;
}
public static void MapProgressColor(ProgressBarHandler handler, IProgress progress)
{
if (progress.ProgressColor != null)
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress)
{
handler.PlatformView.IsEnabled = progress.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for ScrollView on Linux using SkiaScrollView.
/// </summary>
public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView>
{
public static IPropertyMapper<IScrollView, ScrollViewHandler> Mapper =
new PropertyMapper<IScrollView, ScrollViewHandler>(ViewMapper)
{
[nameof(IScrollView.Content)] = MapContent,
[nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility,
[nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility,
[nameof(IScrollView.Orientation)] = MapOrientation,
};
public static CommandMapper<IScrollView, ScrollViewHandler> CommandMapper =
new(ViewCommandMapper)
{
[nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo
};
public ScrollViewHandler() : base(Mapper, CommandMapper)
{
}
public ScrollViewHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
protected override SkiaScrollView CreatePlatformView()
{
return new SkiaScrollView();
}
public static void MapContent(ScrollViewHandler handler, IScrollView scrollView)
{
if (handler.PlatformView == null || handler.MauiContext == null)
return;
var content = scrollView.PresentedContent;
if (content != null)
{
DiagnosticLog.Debug("ScrollViewHandler", $"MapContent: {content.GetType().Name}");
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
{
DiagnosticLog.Debug("ScrollViewHandler", $"Setting content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent;
}
}
else
{
handler.PlatformView.Content = null;
}
}
public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch
{
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch
{
Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always,
Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.Orientation = scrollView.Orientation switch
{
Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal,
Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both,
Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither,
_ => ScrollOrientation.Vertical
};
}
public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args)
{
if (args is ScrollToRequest request)
{
// Instant means no animation, so we pass !Instant for animated parameter
handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant);
}
}
}

View File

@@ -1,106 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for SearchBar control.
/// </summary>
public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
{
public static IPropertyMapper<ISearchBar, SearchBarHandler> Mapper = new PropertyMapper<ISearchBar, SearchBarHandler>(ViewHandler.ViewMapper)
{
[nameof(ISearchBar.Text)] = MapText,
[nameof(ISearchBar.Placeholder)] = MapPlaceholder,
[nameof(ISearchBar.PlaceholderColor)] = MapPlaceholderColor,
[nameof(ISearchBar.TextColor)] = MapTextColor,
[nameof(ISearchBar.Font)] = MapFont,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground,
};
public static CommandMapper<ISearchBar, SearchBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SearchBarHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSearchBar CreatePlatformView() => new SkiaSearchBar();
protected override void ConnectHandler(SkiaSearchBar platformView)
{
base.ConnectHandler(platformView);
platformView.TextChanged += OnTextChanged;
platformView.SearchButtonPressed += OnSearchButtonPressed;
}
protected override void DisconnectHandler(SkiaSearchBar platformView)
{
platformView.TextChanged -= OnTextChanged;
platformView.SearchButtonPressed -= OnSearchButtonPressed;
base.DisconnectHandler(platformView);
}
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (VirtualView != null && VirtualView.Text != e.NewText)
{
VirtualView.Text = e.NewText;
}
}
private void OnSearchButtonPressed(object? sender, EventArgs e)
{
VirtualView?.SearchButtonPressed();
}
public static void MapText(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView.Text != searchBar.Text)
{
handler.PlatformView.Text = searchBar.Text ?? "";
}
}
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
{
handler.PlatformView.Placeholder = searchBar.Placeholder ?? "";
handler.PlatformView.Invalidate();
}
public static void MapPlaceholderColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.PlaceholderColor != null)
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.TextColor != null)
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
{
var font = searchBar.Font;
if (font.Family != null)
handler.PlatformView.FontFamily = font.Family;
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SearchBarHandler handler, ISearchBar searchBar)
{
handler.PlatformView.IsEnabled = searchBar.IsEnabled;
handler.PlatformView.Invalidate();
}
public static void MapBackground(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}

View File

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

View File

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

View File

@@ -1,103 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for Slider control.
/// </summary>
public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
{
public static IPropertyMapper<ISlider, SliderHandler> Mapper = new PropertyMapper<ISlider, SliderHandler>(ViewHandler.ViewMapper)
{
[nameof(ISlider.Minimum)] = MapMinimum,
[nameof(ISlider.Maximum)] = MapMaximum,
[nameof(ISlider.Value)] = MapValue,
[nameof(ISlider.MinimumTrackColor)] = MapMinimumTrackColor,
[nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor,
[nameof(ISlider.ThumbColor)] = MapThumbColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SliderHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSlider CreatePlatformView() => new SkiaSlider();
protected override void ConnectHandler(SkiaSlider platformView)
{
base.ConnectHandler(platformView);
platformView.ValueChanged += OnValueChanged;
platformView.DragStarted += OnDragStarted;
platformView.DragCompleted += OnDragCompleted;
}
protected override void DisconnectHandler(SkiaSlider platformView)
{
platformView.ValueChanged -= OnValueChanged;
platformView.DragStarted -= OnDragStarted;
platformView.DragCompleted -= OnDragCompleted;
base.DisconnectHandler(platformView);
}
private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
{
if (VirtualView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.001)
{
VirtualView.Value = e.NewValue;
}
}
private void OnDragStarted(object? sender, EventArgs e) => VirtualView?.DragStarted();
private void OnDragCompleted(object? sender, EventArgs e) => VirtualView?.DragCompleted();
public static void MapMinimum(SliderHandler handler, ISlider slider)
{
handler.PlatformView.Minimum = slider.Minimum;
handler.PlatformView.Invalidate();
}
public static void MapMaximum(SliderHandler handler, ISlider slider)
{
handler.PlatformView.Maximum = slider.Maximum;
handler.PlatformView.Invalidate();
}
public static void MapValue(SliderHandler handler, ISlider slider)
{
if (Math.Abs(handler.PlatformView.Value - slider.Value) > 0.001)
{
handler.PlatformView.Value = slider.Value;
}
}
public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MinimumTrackColor != null)
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MaximumTrackColor != null)
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapThumbColor(SliderHandler handler, ISlider slider)
{
if (slider.ThumbColor != null)
handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
{
handler.PlatformView.IsEnabled = slider.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -22,6 +22,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
[nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor,
[nameof(ISlider.ThumbColor)] = MapThumbColor,
[nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -48,6 +49,15 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
platformView.ValueChanged += OnValueChanged;
platformView.DragStarted += OnDragStarted;
platformView.DragCompleted += OnDragCompleted;
// Sync properties that may have been set before handler connection
if (VirtualView != null)
{
MapMinimum(this, VirtualView);
MapMaximum(this, VirtualView);
MapValue(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaSlider platformView)
@@ -58,7 +68,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
base.DisconnectHandler(platformView);
}
private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
private void OnValueChanged(object? sender, ValueChangedEventArgs e)
{
if (VirtualView is null || PlatformView is null) return;
@@ -102,18 +112,16 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
{
if (handler.PlatformView is null) return;
// MinimumTrackColor maps to ActiveTrackColor (the filled portion)
if (slider.MinimumTrackColor is not null)
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
handler.PlatformView.MinimumTrackColor = slider.MinimumTrackColor;
}
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{
if (handler.PlatformView is null) return;
// MaximumTrackColor maps to TrackColor (the unfilled portion)
if (slider.MaximumTrackColor is not null)
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
handler.PlatformView.MaximumTrackColor = slider.MaximumTrackColor;
}
public static void MapThumbColor(SliderHandler handler, ISlider slider)
@@ -121,7 +129,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
if (handler.PlatformView is null) return;
if (slider.ThumbColor is not null)
handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor();
handler.PlatformView.ThumbColor = slider.ThumbColor;
}
public static void MapBackground(SliderHandler handler, ISlider slider)
@@ -130,7 +138,14 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
if (slider.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidPaint.Color;
}
}
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = slider.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for Switch control.
/// </summary>
public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
{
public static IPropertyMapper<ISwitch, SwitchHandler> Mapper = new PropertyMapper<ISwitch, SwitchHandler>(ViewHandler.ViewMapper)
{
[nameof(ISwitch.IsOn)] = MapIsOn,
[nameof(ISwitch.TrackColor)] = MapTrackColor,
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
};
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SwitchHandler() : base(Mapper, CommandMapper) { }
protected override SkiaSwitch CreatePlatformView() => new SkiaSwitch();
protected override void ConnectHandler(SkiaSwitch platformView)
{
base.ConnectHandler(platformView);
platformView.Toggled += OnToggled;
}
protected override void DisconnectHandler(SkiaSwitch platformView)
{
platformView.Toggled -= OnToggled;
base.DisconnectHandler(platformView);
}
private void OnToggled(object? sender, ToggledEventArgs e)
{
if (VirtualView != null && VirtualView.IsOn != e.Value)
{
VirtualView.IsOn = e.Value;
}
}
public static void MapIsOn(SwitchHandler handler, ISwitch @switch)
{
if (handler.PlatformView.IsOn != @switch.IsOn)
{
handler.PlatformView.IsOn = @switch.IsOn;
}
}
public static void MapTrackColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.TrackColor != null)
handler.PlatformView.OnTrackColor = @switch.TrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapThumbColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.ThumbColor != null)
handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor();
handler.PlatformView.Invalidate();
}
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
{
handler.PlatformView.IsEnabled = @switch.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

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

View File

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

View File

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

View File

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

196
Handlers/WebViewHandler.cs Normal file
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 Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for WebView control on Linux using WebKitGTK.
/// </summary>
public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
{
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IWebView.Source)] = MapSource,
[nameof(IWebView.UserAgent)] = MapUserAgent,
};
public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
[nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload,
[nameof(IWebView.Eval)] = MapEval,
[nameof(IWebView.EvaluateJavaScriptAsync)] = MapEvaluateJavaScriptAsync,
};
public WebViewHandler() : base(Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaWebView CreatePlatformView()
{
return new SkiaWebView();
}
protected override void ConnectHandler(SkiaWebView platformView)
{
base.ConnectHandler(platformView);
platformView.Navigating += OnNavigating;
platformView.Navigated += OnNavigated;
}
protected override void DisconnectHandler(SkiaWebView platformView)
{
platformView.Navigating -= OnNavigating;
platformView.Navigated -= OnNavigated;
base.DisconnectHandler(platformView);
}
private void OnNavigating(object? sender, Microsoft.Maui.Platform.WebNavigatingEventArgs e)
{
IWebView virtualView = VirtualView;
IWebViewController? controller = virtualView as IWebViewController;
if (controller != null)
{
var args = new Microsoft.Maui.Controls.WebNavigatingEventArgs(
WebNavigationEvent.NewPage,
null,
e.Url);
controller.SendNavigating(args);
}
}
private void OnNavigated(object? sender, Microsoft.Maui.Platform.WebNavigatedEventArgs e)
{
IWebView virtualView = VirtualView;
IWebViewController? controller = virtualView as IWebViewController;
if (controller != null)
{
WebNavigationResult result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
var args = new Microsoft.Maui.Controls.WebNavigatedEventArgs(
WebNavigationEvent.NewPage,
null,
e.Url,
result);
controller.SendNavigated(args);
}
}
public static void MapSource(WebViewHandler handler, IWebView webView)
{
DiagnosticLog.Debug("WebViewHandler", "MapSource called");
if (handler.PlatformView == null)
{
DiagnosticLog.Warn("WebViewHandler", "PlatformView is null!");
return;
}
var source = webView.Source;
DiagnosticLog.Debug("WebViewHandler", $"Source type: {source?.GetType().Name ?? "null"}");
if (source is UrlWebViewSource urlSource)
{
DiagnosticLog.Debug("WebViewHandler", $"Loading URL: {urlSource.Url}");
handler.PlatformView.Source = urlSource.Url ?? "";
}
else if (source is HtmlWebViewSource htmlSource)
{
DiagnosticLog.Debug("WebViewHandler", $"Loading HTML ({htmlSource.Html?.Length ?? 0} chars)");
DiagnosticLog.Debug("WebViewHandler", $"HTML preview: {htmlSource.Html?.Substring(0, Math.Min(100, htmlSource.Html?.Length ?? 0))}...");
handler.PlatformView.Html = htmlSource.Html ?? "";
}
else
{
DiagnosticLog.Debug("WebViewHandler", "Unknown source type or null");
}
}
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.GoBack();
}
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.GoForward();
}
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.Reload();
}
public static void MapUserAgent(WebViewHandler handler, IWebView webView)
{
if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent))
{
handler.PlatformView.UserAgent = webView.UserAgent;
}
}
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
{
if (args is string script)
{
handler.PlatformView?.Eval(script);
}
}
public static void MapEvaluateJavaScriptAsync(WebViewHandler handler, IWebView webView, object? args)
{
// Handle EvaluateJavaScriptAsyncRequest from Microsoft.Maui.Platform namespace
if (args is EvaluateJavaScriptAsyncRequest request)
{
var result = handler.PlatformView?.EvaluateJavaScriptAsync(request.Script);
if (result != null)
{
result.ContinueWith(t =>
{
request.SetResult(t.Result);
});
}
else
{
request.SetResult(null);
}
}
else if (args is string script)
{
// Direct script string
handler.PlatformView?.EvaluateJavaScriptAsync(script);
}
}
}
/// <summary>
/// Request object for async JavaScript evaluation (matches Microsoft.Maui.Platform.EvaluateJavaScriptAsyncRequest).
/// </summary>
public class EvaluateJavaScriptAsyncRequest
{
public string Script { get; }
private readonly System.Threading.Tasks.TaskCompletionSource<string?> _tcs = new();
public EvaluateJavaScriptAsyncRequest(string script)
{
Script = script;
}
public System.Threading.Tasks.Task<string?> Task => _tcs.Task;
public void SetResult(string? result)
{
_tcs.TrySetResult(result);
}
}

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,13 +82,20 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
public static void MapContent(WindowHandler handler, IWindow window)
{
DiagnosticLog.Debug("WindowHandler", $"MapContent - PlatformView={handler.PlatformView != null}");
if (handler.PlatformView is null) return;
var content = window.Content;
DiagnosticLog.Debug("WindowHandler", $"MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
if (content?.Handler?.PlatformView is SkiaView skiaContent)
{
DiagnosticLog.Debug("WindowHandler", $"MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent;
}
else
{
DiagnosticLog.Warn("WindowHandler", $"MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
}
}
public static void MapX(WindowHandler handler, IWindow window)
@@ -141,6 +149,7 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
/// <summary>
/// Skia window wrapper for Linux display servers.
/// Handles rendering of content and popup overlays automatically.
/// </summary>
public class SkiaWindow
{
@@ -164,6 +173,28 @@ public class SkiaWindow
}
}
/// <summary>
/// Renders the window content and popup overlays to the canvas.
/// This should be called by the platform rendering loop.
/// </summary>
public void Render(SKCanvas canvas)
{
// Clear background
canvas.Clear(SKColors.White);
// Draw main content
if (_content != null)
{
_content.Measure(new Size(_width, _height));
_content.Arrange(new Rect(0, 0, _width, _height));
_content.Draw(canvas);
}
// Draw popup overlays on top (dropdowns, date pickers, etc.)
// This ensures popups always render above all other content
SkiaView.DrawPopupOverlays(canvas);
}
public string Title
{
get => _title;

52
Hosting/GtkMauiContext.cs Normal file
View File

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

View File

@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Hosting;
namespace Microsoft.Maui.Platform.Linux.Hosting;
public static class HandlerMappingExtensions
{
public static IMauiHandlersCollection AddHandler<TView, THandler>(this IMauiHandlersCollection handlers)
where TView : class
where THandler : class
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
}
}

View File

@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Animations;
using Animation = Microsoft.Maui.Animations.Animation;
namespace Microsoft.Maui.Platform.Linux.Hosting;
internal class LinuxAnimationManager : IAnimationManager
{
private readonly List<Animation> _animations = new();
private readonly ITicker _ticker;
public double SpeedModifier { get; set; } = 1.0;
public bool AutoStartTicker { get; set; } = true;
public ITicker Ticker => _ticker;
public LinuxAnimationManager(ITicker ticker)
{
_ticker = ticker;
_ticker.Fire = OnTickerFire;
}
public void Add(Animation animation)
{
_animations.Add(animation);
if (AutoStartTicker && !_ticker.IsRunning)
{
_ticker.Start();
}
}
public void Remove(Animation animation)
{
_animations.Remove(animation);
if (_animations.Count == 0 && _ticker.IsRunning)
{
_ticker.Stop();
}
}
private void OnTickerFire()
{
var animationsArray = _animations.ToArray();
foreach (var animation in animationsArray)
{
animation.Tick(0.016 * SpeedModifier);
if (animation.HasFinished)
{
Remove(animation);
}
}
}
}

View File

@@ -1,40 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.ApplicationModel.Communication;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Networking;
using Microsoft.Maui.Platform.Linux.Converters;
using Microsoft.Maui.Platform.Linux.Dispatching;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Storage;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Controls;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for configuring MAUI applications for Linux.
/// </summary>
public static class LinuxMauiAppBuilderExtensions
{
/// <summary>
/// Configures the MAUI application to run on Linux.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
{
return builder.UseLinux(configure: null);
return builder.UseLinux(null);
}
/// <summary>
/// Configures the MAUI application to run on Linux with options.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure)
{
var options = new LinuxApplicationOptions();
configure?.Invoke(options);
// Register dispatcher provider
builder.Services.TryAddSingleton<IDispatcherProvider>(LinuxDispatcherProvider.Instance);
// Register device services
builder.Services.TryAddSingleton<IDeviceInfo>(DeviceInfoService.Instance);
builder.Services.TryAddSingleton<IDeviceDisplay>(DeviceDisplayService.Instance);
builder.Services.TryAddSingleton<IAppInfo>(AppInfoService.Instance);
builder.Services.TryAddSingleton<IConnectivity>(ConnectivityService.Instance);
// Register platform services
builder.Services.TryAddSingleton<ILauncher, LauncherService>();
builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
@@ -47,51 +54,99 @@ public static class LinuxMauiAppBuilderExtensions
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
builder.Services.TryAddSingleton<IEmail, EmailService>();
// Register theming and accessibility services
builder.Services.TryAddSingleton<SystemThemeService>();
builder.Services.TryAddSingleton<HighContrastService>();
// Register accessibility service
builder.Services.TryAddSingleton<IAccessibilityService>(_ => AccessibilityServiceFactory.Instance);
// Register input method service
builder.Services.TryAddSingleton<IInputMethodService>(_ => InputMethodServiceFactory.Instance);
// Register font fallback manager
builder.Services.TryAddSingleton(_ => FontFallbackManager.Instance);
// Register additional Linux-specific services
builder.Services.TryAddSingleton<FolderPickerService>();
builder.Services.TryAddSingleton<NotificationService>();
builder.Services.TryAddSingleton<SystemTrayService>();
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
builder.Services.TryAddSingleton<DragDropService>();
// Register GTK host service
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
// Register type converters for XAML support
RegisterTypeConverters();
// Register Linux-specific handlers
builder.ConfigureMauiHandlers(handlers =>
{
// Phase 1 - MVP controls
handlers.AddHandler<IButton, ButtonHandler>();
handlers.AddHandler<ILabel, LabelHandler>();
handlers.AddHandler<IEntry, EntryHandler>();
handlers.AddHandler<ICheckBox, CheckBoxHandler>();
handlers.AddHandler<ILayout, LayoutHandler>();
handlers.AddHandler<IStackLayout, StackLayoutHandler>();
handlers.AddHandler<IGridLayout, GridHandler>();
// Application handler
handlers.AddHandler<IApplication, ApplicationHandler>();
// Phase 2 - Input controls
handlers.AddHandler<ISlider, SliderHandler>();
handlers.AddHandler<ISwitch, SwitchHandler>();
handlers.AddHandler<IProgress, ProgressBarHandler>();
handlers.AddHandler<IActivityIndicator, ActivityIndicatorHandler>();
handlers.AddHandler<ISearchBar, SearchBarHandler>();
// Core controls
handlers.AddHandler<BoxView, BoxViewHandler>();
handlers.AddHandler<Button, TextButtonHandler>();
handlers.AddHandler<Label, LabelHandler>();
handlers.AddHandler<Entry, EntryHandler>();
handlers.AddHandler<Editor, EditorHandler>();
handlers.AddHandler<CheckBox, CheckBoxHandler>();
handlers.AddHandler<Switch, SwitchHandler>();
handlers.AddHandler<Slider, SliderHandler>();
handlers.AddHandler<Stepper, StepperHandler>();
handlers.AddHandler<RadioButton, RadioButtonHandler>();
// Phase 2 - Image & Graphics
handlers.AddHandler<IImage, ImageHandler>();
handlers.AddHandler<IImageButton, ImageButtonHandler>();
handlers.AddHandler<IGraphicsView, GraphicsViewHandler>();
// Layout controls
handlers.AddHandler<Grid, GridHandler>();
handlers.AddHandler<StackLayout, StackLayoutHandler>();
handlers.AddHandler<VerticalStackLayout, StackLayoutHandler>();
handlers.AddHandler<HorizontalStackLayout, StackLayoutHandler>();
handlers.AddHandler<AbsoluteLayout, LayoutHandler>();
handlers.AddHandler<FlexLayout, FlexLayoutHandler>();
handlers.AddHandler<ScrollView, ScrollViewHandler>();
handlers.AddHandler<Frame, FrameHandler>();
handlers.AddHandler<Border, BorderHandler>();
handlers.AddHandler<ContentView, BorderHandler>();
handlers.AddHandler<RefreshView, RefreshViewHandler>();
// Phase 3 - Collection Views
// Picker controls
handlers.AddHandler<Picker, PickerHandler>();
handlers.AddHandler<DatePicker, DatePickerHandler>();
handlers.AddHandler<TimePicker, TimePickerHandler>();
handlers.AddHandler<SearchBar, SearchBarHandler>();
// Progress & Activity
handlers.AddHandler<ProgressBar, ProgressBarHandler>();
handlers.AddHandler<ActivityIndicator, ActivityIndicatorHandler>();
// Image & Graphics
handlers.AddHandler<Image, ImageHandler>();
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
// Web - use GtkWebViewHandler
handlers.AddHandler<WebView, GtkWebViewHandler>();
// Collection Views
handlers.AddHandler<CollectionView, CollectionViewHandler>();
handlers.AddHandler<ListView, CollectionViewHandler>();
handlers.AddHandler<CarouselView, CarouselViewHandler>();
handlers.AddHandler<IndicatorView, IndicatorViewHandler>();
handlers.AddHandler<SwipeView, SwipeViewHandler>();
// Phase 4 - Pages & Navigation
// Pages & Navigation
handlers.AddHandler<Page, PageHandler>();
handlers.AddHandler<ContentPage, ContentPageHandler>();
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
handlers.AddHandler<Shell, ShellHandler>();
handlers.AddHandler<FlyoutPage, FlyoutPageHandler>();
handlers.AddHandler<TabbedPage, TabbedPageHandler>();
// Phase 5 - Advanced Controls
handlers.AddHandler<IPicker, PickerHandler>();
handlers.AddHandler<IDatePicker, DatePickerHandler>();
handlers.AddHandler<ITimePicker, TimePickerHandler>();
handlers.AddHandler<IEditor, EditorHandler>();
// Phase 7 - Additional Controls
handlers.AddHandler<IStepper, StepperHandler>();
handlers.AddHandler<IRadioButton, RadioButtonHandler>();
handlers.AddHandler<IBorderView, BorderHandler>();
// Window handler
handlers.AddHandler<IWindow, WindowHandler>();
// Application & Window
handlers.AddHandler<Application, ApplicationHandler>();
handlers.AddHandler<Microsoft.Maui.Controls.Window, WindowHandler>();
});
// Store options for later use
@@ -99,22 +154,12 @@ public static class LinuxMauiAppBuilderExtensions
return builder;
}
}
/// <summary>
/// Handler registration extensions.
/// </summary>
public static class HandlerMappingExtensions
{
/// <summary>
/// Adds a handler for the specified view type.
/// </summary>
public static IMauiHandlersCollection AddHandler<TView, THandler>(
this IMauiHandlersCollection handlers)
where TView : class
where THandler : class
private static void RegisterTypeConverters()
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter)));
TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter)));
}
}

View File

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

View File

@@ -2,41 +2,159 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Entry point for running MAUI applications on Linux.
/// </summary>
public static class LinuxProgramHost
{
/// <summary>
/// Runs the MAUI application on Linux.
/// </summary>
/// <typeparam name="TApp">The application type.</typeparam>
/// <param name="args">Command line arguments.</param>
public static void Run<TApp>(string[] args) where TApp : class, IApplication, new()
{
Run<TApp>(args, null);
}
/// <summary>
/// Runs the MAUI application on Linux with additional configuration.
/// </summary>
/// <typeparam name="TApp">The application type.</typeparam>
/// <param name="args">Command line arguments.</param>
/// <param name="configure">Optional builder configuration action.</param>
public static void Run<TApp>(string[] args, Action<MauiAppBuilder>? configure) where TApp : class, IApplication, new()
{
// Build the MAUI application
var builder = MauiApp.CreateBuilder();
builder.UseLinux();
configure?.Invoke(builder);
builder.UseMauiApp<TApp>();
var mauiApp = builder.Build();
// Get application options
var options = mauiApp.Services.GetService<LinuxApplicationOptions>()
?? new LinuxApplicationOptions();
ParseCommandLineOptions(args, options);
// Initialize GTK for WebView support
GtkHostService.Instance.Initialize(options.Title ?? "MAUI Application", options.Width, options.Height);
DiagnosticLog.Debug("LinuxProgramHost", "GTK initialized for WebView support");
// Create Linux application
using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options);
// Create comprehensive demo UI with ALL controls
var rootView = CreateComprehensiveDemo();
linuxApp.RootView = rootView;
// Create MAUI context
var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp);
// Get the MAUI application instance
var application = mauiApp.Services.GetService<IApplication>();
// Ensure Application.Current is set - required for Shell.Current to work
if (application is Application app && Application.Current == null)
{
// Use reflection to set Current since it has a protected setter
var currentProperty = typeof(Application).GetProperty("Current");
currentProperty?.SetValue(null, app);
}
// Try to render the application's main page
SkiaView? rootView = null;
if (application != null)
{
rootView = RenderApplication(application, mauiContext, options);
}
// Fallback to demo if no application view is available
if (rootView == null)
{
DiagnosticLog.Warn("LinuxProgramHost", "No application page found. Showing demo UI.");
rootView = CreateDemoView();
}
linuxApp.RootView = rootView;
linuxApp.Run();
}
/// <summary>
/// Renders the MAUI application and returns the root SkiaView.
/// </summary>
private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options)
{
try
{
// For Applications, we need to create a window
if (application is Application app)
{
Page? mainPage = app.MainPage;
// If no MainPage set, check for windows
if (mainPage == null && application.Windows.Count > 0)
{
var existingWindow = application.Windows[0];
if (existingWindow.Content is Page page)
{
mainPage = page;
}
}
if (mainPage != null)
{
// Create a MAUI Window and add it to the application
// This ensures Shell.Current works properly (it reads from Application.Current.Windows[0].Page)
if (app.Windows.Count == 0)
{
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
// Try OpenWindow first
app.OpenWindow(mauiWindow);
// If that didn't work, use reflection to add directly to _windows
if (app.Windows.Count == 0)
{
var windowsField = typeof(Application).GetField("_windows",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (windowsField?.GetValue(app) is System.Collections.IList windowsList)
{
windowsList.Add(mauiWindow);
}
}
}
return RenderPage(mainPage, mauiContext);
}
}
return null;
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxProgramHost", $"Error rendering application: {ex.Message}");
DiagnosticLog.Error("LinuxProgramHost", ex.StackTrace ?? "");
return null;
}
}
/// <summary>
/// Renders a MAUI Page to a SkiaView.
/// </summary>
private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext)
{
var renderer = new LinuxViewRenderer(mauiContext);
return renderer.RenderPage(page);
}
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
{
for (int i = 0; i < args.Length; i++)
@@ -54,70 +172,77 @@ public static class LinuxProgramHost
options.Height = h;
i++;
break;
case "--demo":
// Force demo mode
options.ForceDemo = true;
break;
}
}
}
private static SkiaView CreateComprehensiveDemo()
/// <summary>
/// Creates a demo view showcasing all controls.
/// </summary>
public static SkiaView CreateDemoView()
{
// Create scrollable container
var scroll = new SkiaScrollView();
var root = new SkiaStackLayout
{
Orientation = StackOrientation.Vertical,
Spacing = 15,
BackgroundColor = new SKColor(0xF5, 0xF5, 0xF5)
BackgroundColor = Color.FromRgb(0xF5, 0xF5, 0xF5)
};
root.Padding = new SKRect(20, 20, 20, 20);
root.Padding = new Thickness(20, 20, 20, 20);
// ========== TITLE ==========
root.AddChild(new SkiaLabel
{
Text = "MAUI Linux Control Demo",
FontSize = 28,
TextColor = new SKColor(0x1A, 0x23, 0x7E),
IsBold = true
root.AddChild(new SkiaLabel
{
Text = "OpenMaui Linux Control Demo",
FontSize = 28,
TextColor = Color.FromRgb(0x1A, 0x23, 0x7E),
FontAttributes = FontAttributes.Bold
});
root.AddChild(new SkiaLabel
{
Text = "All controls rendered using SkiaSharp on X11",
FontSize = 14,
TextColor = SKColors.Gray
root.AddChild(new SkiaLabel
{
Text = "All controls rendered using SkiaSharp on X11",
FontSize = 14,
TextColor = Colors.Gray
});
// ========== LABELS SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Labels"));
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black });
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true });
labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = SKColors.Gray, IsItalic = true });
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = new SKColor(0xE9, 0x1E, 0x63) });
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = Colors.Black });
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = Colors.Black, FontAttributes = FontAttributes.Bold });
labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = Colors.Gray, FontAttributes = FontAttributes.Italic });
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = Color.FromRgb(0xE9, 0x1E, 0x63) });
root.AddChild(labelSection);
// ========== BUTTONS SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Buttons"));
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3);
btnPrimary.TextColor = SKColors.White;
btnPrimary.BackgroundColor = Color.FromRgb(0x21, 0x96, 0xF3);
btnPrimary.TextColor = Colors.White;
var clickCount = 0;
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
buttonSection.AddChild(btnPrimary);
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50);
btnSuccess.TextColor = SKColors.White;
btnSuccess.BackgroundColor = Color.FromRgb(0x4C, 0xAF, 0x50);
btnSuccess.TextColor = Colors.White;
buttonSection.AddChild(btnSuccess);
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36);
btnDanger.TextColor = SKColors.White;
btnDanger.BackgroundColor = Color.FromRgb(0xF4, 0x43, 0x36);
btnDanger.TextColor = Colors.White;
buttonSection.AddChild(btnDanger);
root.AddChild(buttonSection);
// ========== ENTRY SECTION ==========
@@ -130,7 +255,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("SearchBar"));
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray };
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = Colors.Gray };
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
root.AddChild(searchBar);
@@ -139,11 +264,11 @@ public static class LinuxProgramHost
// ========== EDITOR SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Editor (Multi-line)"));
var editor = new SkiaEditor
{
Placeholder = "Enter multiple lines of text...",
var editor = new SkiaEditor
{
Placeholder = "Enter multiple lines of text...",
FontSize = 14,
BackgroundColor = SKColors.White
BackgroundColor = Colors.White
};
root.AddChild(editor);
@@ -205,7 +330,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSectionHeader("ProgressBar"));
var progress = new SkiaProgressBar { Progress = 0.7f };
root.AddChild(progress);
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray });
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = Colors.Gray });
// ========== ACTIVITYINDICATOR SECTION ==========
root.AddChild(CreateSeparator());
@@ -213,7 +338,7 @@ public static class LinuxProgramHost
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var activity = new SkiaActivityIndicator { IsRunning = true };
activitySection.AddChild(activity);
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray });
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = Colors.Gray });
root.AddChild(activitySection);
// ========== PICKER SECTION ==========
@@ -221,7 +346,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
var picker = new SkiaPicker { Title = "Select an item" };
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
root.AddChild(picker);
root.AddChild(pickerLabel);
@@ -230,7 +355,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("DatePicker"));
var datePicker = new SkiaDatePicker { Date = DateTime.Today };
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = SKColors.Gray };
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = Colors.Gray };
datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}";
root.AddChild(datePicker);
root.AddChild(dateLabel);
@@ -239,7 +364,7 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("TimePicker"));
var timePicker = new SkiaTimePicker();
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = SKColors.Gray };
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = Colors.Gray };
timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}";
root.AddChild(timePicker);
root.AddChild(timeLabel);
@@ -251,18 +376,18 @@ public static class LinuxProgramHost
{
CornerRadius = 8,
StrokeThickness = 2,
Stroke = new SKColor(0x21, 0x96, 0xF3),
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD)
Stroke = Color.FromRgb(0x21, 0x96, 0xF3),
BackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD)
};
border.SetPadding(15);
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = new SKColor(0x1A, 0x23, 0x7E) });
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = Color.FromRgb(0x1A, 0x23, 0x7E) });
root.AddChild(border);
// ========== FRAME SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Frame (with shadow)"));
var frame = new SkiaFrame();
frame.BackgroundColor = SKColors.White;
frame.BackgroundColor = Colors.White;
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
root.AddChild(frame);
@@ -276,8 +401,8 @@ public static class LinuxProgramHost
Footer = "End of list"
};
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
collectionView.SelectionChanged += (s, e) =>
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
collectionView.SelectionChanged += (s, e) =>
{
var selected = e.CurrentSelection.FirstOrDefault();
collectionLabel.Text = $"Selected: {selected}";
@@ -289,23 +414,20 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("ImageButton"));
var imageButtonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
// Create ImageButton with a generated icon (since we don't have image files)
var imgBtn = new SkiaImageButton
{
CornerRadius = 8,
StrokeColor = new SKColor(0x21, 0x96, 0xF3),
StrokeColor = Color.FromRgb(0x21, 0x96, 0xF3),
StrokeThickness = 1,
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD),
PaddingLeft = 10,
PaddingRight = 10,
PaddingTop = 10,
PaddingBottom = 10
ImageBackgroundColor = Color.FromRgb(0xE3, 0xF2, 0xFD),
Padding = new Thickness(10)
};
// Generate a simple star icon bitmap
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
imgBtn.Bitmap = iconBitmap;
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray };
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = Colors.Gray };
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
imageButtonSection.AddChild(imgBtn);
imageButtonSection.AddChild(imgBtnLabel);
@@ -315,29 +437,29 @@ public static class LinuxProgramHost
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Image"));
var imageSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
// Create Image with a generated sample image
var img = new SkiaImage();
var sampleBitmap = CreateSampleImage(80, 60);
img.Bitmap = sampleBitmap;
imageSection.AddChild(img);
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray });
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = Colors.Gray });
root.AddChild(imageSection);
// ========== FOOTER ==========
root.AddChild(CreateSeparator());
root.AddChild(new SkiaLabel
{
Text = "All 25+ controls are interactive - try them all!",
FontSize = 16,
TextColor = new SKColor(0x4C, 0xAF, 0x50),
IsBold = true
root.AddChild(new SkiaLabel
{
Text = "All 25+ controls are interactive - try them all!",
FontSize = 16,
TextColor = Color.FromRgb(0x4C, 0xAF, 0x50),
FontAttributes = FontAttributes.Bold
});
root.AddChild(new SkiaLabel
{
Text = "Scroll down to see more controls",
FontSize = 12,
TextColor = SKColors.Gray
root.AddChild(new SkiaLabel
{
Text = "Scroll down to see more controls",
FontSize = 12,
TextColor = Colors.Gray
});
scroll.Content = root;
@@ -350,14 +472,14 @@ public static class LinuxProgramHost
{
Text = text,
FontSize = 18,
TextColor = new SKColor(0x37, 0x47, 0x4F),
IsBold = true
TextColor = Color.FromRgb(0x37, 0x47, 0x4F),
FontAttributes = FontAttributes.Bold
};
}
private static SkiaView CreateSeparator()
{
var sep = new SkiaLabel { Text = "", BackgroundColor = new SKColor(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
var sep = new SkiaLabel { Text = "", BackgroundColor = Color.FromRgb(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
return sep;
}

47
Hosting/LinuxTicker.cs Normal file
View File

@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Animations;
namespace Microsoft.Maui.Platform.Linux.Hosting;
internal class LinuxTicker : ITicker
{
private Timer? _timer;
private bool _isRunning;
private int _maxFps = 60;
public bool IsRunning => _isRunning;
public bool SystemEnabled => true;
public int MaxFps
{
get => _maxFps;
set => _maxFps = Math.Max(1, Math.Min(120, value));
}
public Action? Fire { get; set; }
public void Start()
{
if (!_isRunning)
{
_isRunning = true;
var period = TimeSpan.FromMilliseconds(1000.0 / _maxFps);
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, period);
}
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Fire?.Invoke();
}
}

View File

@@ -0,0 +1,564 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Renders MAUI views to Skia platform views.
/// Handles the conversion of the view hierarchy.
/// </summary>
public class LinuxViewRenderer
{
private readonly IMauiContext _mauiContext;
/// <summary>
/// Static reference to the current MAUI Shell for navigation support.
/// Used when Shell.Current is not available through normal lifecycle.
/// </summary>
public static Shell? CurrentMauiShell { get; private set; }
/// <summary>
/// Static reference to the current SkiaShell for navigation updates.
/// </summary>
public static SkiaShell? CurrentSkiaShell { get; private set; }
/// <summary>
/// Navigate to a route using the SkiaShell directly.
/// Use this instead of Shell.Current.GoToAsync on Linux.
/// </summary>
/// <param name="route">The route to navigate to (e.g., "Buttons" or "//Buttons")</param>
/// <returns>True if navigation succeeded</returns>
public static bool NavigateToRoute(string route)
{
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell is null");
return false;
}
// Clean up the route - remove leading // or /
var cleanRoute = route.TrimStart('/');
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Navigating to: {cleanRoute}");
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{
var section = CurrentSkiaShell.Sections[i];
if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) ||
section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Found section {i}: {section.Title}");
CurrentSkiaShell.NavigateToSection(i);
return true;
}
}
DiagnosticLog.Warn("LinuxViewRenderer", $"NavigateToRoute: Route not found: {cleanRoute}");
return false;
}
/// <summary>
/// Current renderer instance for page rendering.
/// </summary>
public static LinuxViewRenderer? CurrentRenderer { get; set; }
/// <summary>
/// Pushes a page onto the navigation stack.
/// </summary>
/// <param name="page">The page to push</param>
/// <returns>True if successful</returns>
public static bool PushPage(Page page)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"PushPage: Pushing page: {page.GetType().Name}");
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentSkiaShell is null");
return false;
}
if (CurrentRenderer == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentRenderer is null");
return false;
}
try
{
// Render the page through the proper handler system
// This ensures all properties (including BackgroundColor via AppThemeBinding) are mapped
var skiaPage = CurrentRenderer.RenderPage(page);
if (skiaPage == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: Failed to render page through handler");
return false;
}
// Push onto SkiaShell's navigation stack
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail");
DiagnosticLog.Debug("LinuxViewRenderer", "PushPage: Successfully pushed page via handler system");
return true;
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxViewRenderer", "PushPage failed", ex);
return false;
}
}
/// <summary>
/// Pops the current page from the navigation stack.
/// </summary>
/// <returns>True if successful</returns>
public static bool PopPage()
{
DiagnosticLog.Debug("LinuxViewRenderer", "PopPage: Popping page");
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PopPage: CurrentSkiaShell is null");
return false;
}
return CurrentSkiaShell.PopAsync();
}
public LinuxViewRenderer(IMauiContext mauiContext)
{
_mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext));
// Store reference for push/pop navigation
CurrentRenderer = this;
}
/// <summary>
/// Renders a MAUI page and returns the corresponding SkiaView.
/// </summary>
public SkiaView? RenderPage(Page page)
{
if (page == null)
return null;
// Special handling for Shell - Shell is our navigation container
if (page is Shell shell)
{
return RenderShell(shell);
}
// Set handler context
page.Handler?.DisconnectHandler();
var handler = page.ToHandler(_mauiContext);
// The handler's property mappers (e.g., ContentPageHandler.MapContent)
// already set up the content and child handlers - no need to re-render here.
// Re-rendering would disconnect the existing handler hierarchy.
if (handler.PlatformView is SkiaView skiaPage)
{
return skiaPage;
}
return null;
}
/// <summary>
/// Renders a MAUI Shell with all its navigation structure.
/// </summary>
private SkiaShell RenderShell(Shell shell)
{
// Store reference for navigation - Shell.Current is computed from Application.Current.Windows
// Our platform handles navigation through SkiaShell directly
CurrentMauiShell = shell;
var skiaShell = new SkiaShell
{
Title = shell.Title ?? "App",
FlyoutBehavior = shell.FlyoutBehavior switch
{
FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout,
FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
_ => ShellFlyoutBehavior.Flyout
},
MauiShell = shell
};
// Apply shell colors based on theme
ApplyShellColors(skiaShell, shell);
// Render flyout header if present
if (shell.FlyoutHeader is View headerView)
{
var skiaHeader = RenderView(headerView);
if (skiaHeader != null)
{
skiaShell.FlyoutHeaderView = skiaHeader;
skiaShell.FlyoutHeaderHeight = (float)(headerView.HeightRequest > 0 ? headerView.HeightRequest : 140.0);
}
}
// Render flyout footer if present, otherwise use version text
if (shell.FlyoutFooter is View footerView)
{
var skiaFooter = RenderView(footerView);
if (skiaFooter != null)
{
skiaShell.FlyoutFooterView = skiaFooter;
skiaShell.FlyoutFooterHeight = (float)(footerView.HeightRequest > 0 ? footerView.HeightRequest : 40.0);
}
}
else
{
// Fallback: use assembly version as footer text
var version = Assembly.GetEntryAssembly()?.GetName().Version;
skiaShell.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}";
}
// Process shell items into sections
foreach (var item in shell.Items)
{
ProcessShellItem(skiaShell, item);
}
// Store reference to SkiaShell for navigation
CurrentSkiaShell = skiaShell;
// Set up content renderer and color refresher delegates
skiaShell.ContentRenderer = CreateShellContentPage;
skiaShell.ColorRefresher = ApplyShellColors;
// Subscribe to MAUI Shell navigation events to update SkiaShell
shell.Navigated += OnShellNavigated;
shell.Navigating += (s, e) => DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Navigating: {e.Target}");
DiagnosticLog.Debug("LinuxViewRenderer", $"Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
for (int i = 0; i < skiaShell.Sections.Count; i++)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'");
}
return skiaShell;
}
/// <summary>
/// Applies shell colors based on the current theme (dark/light mode).
/// </summary>
private static void ApplyShellColors(SkiaShell skiaShell, Shell shell)
{
bool isDark = Application.Current?.UserAppTheme == AppTheme.Dark;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Theme is: {(isDark ? "Dark" : "Light")}");
// Flyout background color
if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent)
{
skiaShell.FlyoutBackgroundColor = shell.FlyoutBackgroundColor;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
}
else
{
skiaShell.FlyoutBackgroundColor = isDark
? Color.FromRgb(30, 30, 30)
: Color.FromRgb(255, 255, 255);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
}
// Flyout text color
skiaShell.FlyoutTextColor = isDark
? Color.FromRgb(224, 224, 224)
: Color.FromRgb(33, 33, 33);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutTextColor: {skiaShell.FlyoutTextColor}");
// Content background color
skiaShell.ContentBackgroundColor = isDark
? Color.FromRgb(18, 18, 18)
: Color.FromRgb(250, 250, 250);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
// NavBar background color
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
{
skiaShell.NavBarBackgroundColor = shell.BackgroundColor;
}
else
{
skiaShell.NavBarBackgroundColor = Color.FromRgb(33, 150, 243); // Material blue
}
}
/// <summary>
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
/// </summary>
private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}");
if (CurrentSkiaShell == null || CurrentMauiShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell or CurrentMauiShell is null");
return;
}
// Get the current route from the Shell
var currentState = CurrentMauiShell.CurrentState;
var location = currentState?.Location?.OriginalString ?? "";
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
// Find the matching section in SkiaShell by route
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{
var section = CurrentSkiaShell.Sections[i];
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by route! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex)
{
CurrentSkiaShell.NavigateToSection(i);
}
return;
}
if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by title! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex)
{
CurrentSkiaShell.NavigateToSection(i);
}
return;
}
}
DiagnosticLog.Warn("LinuxViewRenderer", $"Navigation: No matching section found for location: {location}");
}
/// <summary>
/// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections.
/// </summary>
private void ProcessShellItem(SkiaShell skiaShell, ShellItem item)
{
if (item is FlyoutItem flyoutItem)
{
// Each FlyoutItem becomes a section
var section = new ShellSection
{
Title = flyoutItem.Title ?? "",
Route = flyoutItem.Route ?? flyoutItem.Title ?? ""
};
// Process the items within the FlyoutItem
foreach (var shellSection in flyoutItem.Items)
{
foreach (var content in shellSection.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
// Create the page content
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
}
// If there's only one item, use it as the main section content
if (section.Items.Count == 1)
{
section.Title = section.Items[0].Title;
}
skiaShell.AddSection(section);
}
else if (item is TabBar tabBar)
{
// TabBar items get their own sections
foreach (var tab in tabBar.Items)
{
var section = new ShellSection
{
Title = tab.Title ?? "",
Route = tab.Route ?? ""
};
foreach (var content in tab.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? tab.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
skiaShell.AddSection(section);
}
}
else
{
// Generic ShellItem
var section = new ShellSection
{
Title = item.Title ?? "",
Route = item.Route ?? ""
};
foreach (var shellSection in item.Items)
{
foreach (var content in shellSection.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
}
skiaShell.AddSection(section);
}
}
/// <summary>
/// Creates the page content for a ShellContent.
/// </summary>
private SkiaView? CreateShellContentPage(Controls.ShellContent content)
{
try
{
// Try to create the page from the content template
Page? page = null;
if (content.ContentTemplate != null)
{
page = content.ContentTemplate.CreateContent() as Page;
}
if (page == null && content.Content is Page contentPage)
{
page = contentPage;
}
if (page is ContentPage cp && cp.Content != null)
{
// Wrap in a scroll view if not already scrollable
var contentView = RenderView(cp.Content);
if (contentView != null)
{
// Get page background color if set
Color? bgColor = null;
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
{
bgColor = cp.BackgroundColor;
DiagnosticLog.Debug("LinuxViewRenderer", $"CreateShellContentPage: Page BackgroundColor: {bgColor}");
}
if (contentView is SkiaScrollView scrollView)
{
if (bgColor != null)
{
scrollView.BackgroundColor = bgColor;
}
return scrollView;
}
else
{
var newScrollView = new SkiaScrollView
{
Content = contentView
};
if (bgColor != null)
{
newScrollView.BackgroundColor = bgColor;
}
return newScrollView;
}
}
}
}
catch (Exception)
{
// Silently handle template creation errors
}
return null;
}
/// <summary>
/// Renders a MAUI view and returns the corresponding SkiaView.
/// </summary>
public SkiaView? RenderView(IView view)
{
if (view == null)
return null;
try
{
// Disconnect any existing handler
if (view is Element element && element.Handler != null)
{
element.Handler.DisconnectHandler();
}
// Create handler for the view
// The handler's ConnectHandler and property mappers handle child views automatically
var handler = view.ToHandler(_mauiContext);
if (handler?.PlatformView is not SkiaView skiaView)
{
// If no Skia handler, create a fallback
return CreateFallbackView(view);
}
// Handlers manage their own children via ConnectHandler and property mappers
// No manual child rendering needed here - that caused "View already has a parent" errors
return skiaView;
}
catch (Exception)
{
return CreateFallbackView(view);
}
}
/// <summary>
/// Creates a fallback view for unsupported view types.
/// </summary>
private SkiaView CreateFallbackView(IView view)
{
// For views without handlers, create a placeholder
return new SkiaLabel
{
Text = $"[{view.GetType().Name}]",
TextColor = Colors.Gray,
FontSize = 12
};
}
}

View File

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

View File

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

View File

@@ -153,4 +153,154 @@ public static class KeyMapping
return modifiers;
}
// Linux evdev keycode to Key mapping (used by Wayland)
private static readonly Dictionary<uint, Key> LinuxKeycodeToKey = new()
{
// Top row
[1] = Key.Escape,
[2] = Key.D1, [3] = Key.D2, [4] = Key.D3, [5] = Key.D4, [6] = Key.D5,
[7] = Key.D6, [8] = Key.D7, [9] = Key.D8, [10] = Key.D9, [11] = Key.D0,
[12] = Key.Minus, [13] = Key.Equals, [14] = Key.Backspace, [15] = Key.Tab,
// QWERTY row
[16] = Key.Q, [17] = Key.W, [18] = Key.E, [19] = Key.R, [20] = Key.T,
[21] = Key.Y, [22] = Key.U, [23] = Key.I, [24] = Key.O, [25] = Key.P,
[26] = Key.LeftBracket, [27] = Key.RightBracket, [28] = Key.Enter,
// Control and ASDF row
[29] = Key.Control,
[30] = Key.A, [31] = Key.S, [32] = Key.D, [33] = Key.F, [34] = Key.G,
[35] = Key.H, [36] = Key.J, [37] = Key.K, [38] = Key.L,
[39] = Key.Semicolon, [40] = Key.Quote, [41] = Key.Grave,
// Shift and ZXCV row
[42] = Key.Shift, [43] = Key.Backslash,
[44] = Key.Z, [45] = Key.X, [46] = Key.C, [47] = Key.V, [48] = Key.B,
[49] = Key.N, [50] = Key.M,
[51] = Key.Comma, [52] = Key.Period, [53] = Key.Slash, [54] = Key.Shift,
// Bottom row
[55] = Key.NumPadMultiply, [56] = Key.Alt, [57] = Key.Space,
[58] = Key.CapsLock,
// Function keys
[59] = Key.F1, [60] = Key.F2, [61] = Key.F3, [62] = Key.F4,
[63] = Key.F5, [64] = Key.F6, [65] = Key.F7, [66] = Key.F8,
[67] = Key.F9, [68] = Key.F10,
// NumLock and numpad
[69] = Key.NumLock, [70] = Key.ScrollLock,
[71] = Key.NumPad7, [72] = Key.NumPad8, [73] = Key.NumPad9, [74] = Key.NumPadSubtract,
[75] = Key.NumPad4, [76] = Key.NumPad5, [77] = Key.NumPad6, [78] = Key.NumPadAdd,
[79] = Key.NumPad1, [80] = Key.NumPad2, [81] = Key.NumPad3,
[82] = Key.NumPad0, [83] = Key.NumPadDecimal,
// More function keys
[87] = Key.F11, [88] = Key.F12,
// Extended keys
[96] = Key.Enter, // NumPad Enter
[97] = Key.Control, // Right Control
[98] = Key.NumPadDivide,
[99] = Key.PrintScreen,
[100] = Key.Alt, // Right Alt
[102] = Key.Home,
[103] = Key.Up,
[104] = Key.PageUp,
[105] = Key.Left,
[106] = Key.Right,
[107] = Key.End,
[108] = Key.Down,
[109] = Key.PageDown,
[110] = Key.Insert,
[111] = Key.Delete,
[119] = Key.Pause,
[125] = Key.Super, // Left Super (Windows key)
[126] = Key.Super, // Right Super
[127] = Key.Menu,
};
/// <summary>
/// Converts a Linux evdev keycode to a MAUI Key.
/// Used for Wayland input where keycodes are offset by 8 from X11 keycodes.
/// </summary>
public static Key FromLinuxKeycode(uint keycode)
{
// Wayland uses evdev keycodes, X11 uses keycodes + 8
// If caller added 8, subtract it
var evdevCode = keycode >= 8 ? keycode - 8 : keycode;
if (LinuxKeycodeToKey.TryGetValue(evdevCode, out var key))
return key;
return Key.Unknown;
}
/// <summary>
/// Converts a Key to its character representation, if applicable.
/// </summary>
public static char? ToChar(Key key, KeyModifiers modifiers)
{
bool shift = modifiers.HasFlag(KeyModifiers.Shift);
bool capsLock = modifiers.HasFlag(KeyModifiers.CapsLock);
bool upper = shift ^ capsLock;
// Letters
if (key >= Key.A && key <= Key.Z)
{
char ch = (char)('a' + (key - Key.A));
return upper ? char.ToUpper(ch) : ch;
}
// Numbers (with shift gives symbols)
if (key >= Key.D0 && key <= Key.D9)
{
if (shift)
{
return (key - Key.D0) switch
{
0 => ')',
1 => '!',
2 => '@',
3 => '#',
4 => '$',
5 => '%',
6 => '^',
7 => '&',
8 => '*',
9 => '(',
_ => null
};
}
return (char)('0' + (key - Key.D0));
}
// NumPad numbers
if (key >= Key.NumPad0 && key <= Key.NumPad9)
return (char)('0' + (key - Key.NumPad0));
// Punctuation
return key switch
{
Key.Space => ' ',
Key.Comma => shift ? '<' : ',',
Key.Period => shift ? '>' : '.',
Key.Slash => shift ? '?' : '/',
Key.Semicolon => shift ? ':' : ';',
Key.Quote => shift ? '"' : '\'',
Key.LeftBracket => shift ? '{' : '[',
Key.RightBracket => shift ? '}' : ']',
Key.Backslash => shift ? '|' : '\\',
Key.Minus => shift ? '_' : '-',
Key.Equals => shift ? '+' : '=',
Key.Grave => shift ? '~' : '`',
Key.NumPadAdd => '+',
Key.NumPadSubtract => '-',
Key.NumPadMultiply => '*',
Key.NumPadDivide => '/',
Key.NumPadDecimal => '.',
_ => null
};
}
}

View File

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

345
Interop/WebKitGtk.cs Normal file
View File

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

239
Interop/X11.cs Normal file
View File

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

View File

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

23
Interop/XButtonEvent.cs Normal file
View File

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

13
Interop/XClassHint.cs Normal file
View File

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

View File

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

View File

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

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