89 Commits

Author SHA1 Message Date
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
321 changed files with 34054 additions and 9195 deletions

View File

@@ -21,15 +21,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version from tag
id: version
shell: pwsh
run: |
$tag = "${{ github.ref_name }}"
$version = $tag -replace '^v', ''
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
echo "Building version: $version"
- name: Restore dependencies
run: C:\dotnet\dotnet.exe restore
@@ -38,9 +29,13 @@ jobs:
- name: Run tests
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
continue-on-error: true
- name: Pack NuGet package
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg /p:PackageVersion=${{ steps.version.outputs.VERSION }}
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
- name: Publish to NuGet.org
run: C:\dotnet\dotnet.exe nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
run: |
foreach ($pkg in (Get-ChildItem .\nupkg\*.nupkg)) {
C:\dotnet\dotnet.exe nuget push $pkg.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
}

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ coverage*.xml
# Publish output
publish/
mauiplan.md

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

297
CLAUDE.md Normal file
View File

@@ -0,0 +1,297 @@
# CLAUDE.md - OpenMaui XAML Reconstruction
## CURRENT TASK: Reconstruct XAML from Decompiled Code
The sample applications (ShellDemo, TodoApp, XamlBrowser) were recovered from decompiled DLLs. The XAML files were compiled away - we have only the generated `InitializeComponent()` code. **Screenshots will be provided** to help verify visual accuracy.
---
## Project Locations
| What | Path |
|------|------|
| **Main codebase** | `/Users/nible/Documents/GitHub/maui-linux-main/` |
| **Samples (target)** | `/Users/nible/Documents/GitHub/maui-linux-main/samples_temp/` |
| **Decompiled samples** | `/Users/nible/Documents/GitHub/recovered/source/` |
---
## Git Branch
**Work on `final` branch.** Commit frequently.
```bash
git branch # Should show: * final
```
---
## XAML Reconstruction Overview
### ShellDemo (10 pages + shell + app)
| File | Status | Notes |
|------|--------|-------|
| App.xaml | [ ] | Colors, Styles (ThemedEntry, TitleLabel, etc.) |
| AppShell.xaml | [ ] | Shell with FlyoutHeader, 9 FlyoutItems |
| HomePage.xaml | [ ] | Welcome screen with logo |
| ButtonsPage.xaml | [ ] | Button demos |
| TextInputPage.xaml | [ ] | Entry/Editor demos |
| SelectionPage.xaml | [ ] | CheckBox, Switch, RadioButton demos |
| PickersPage.xaml | [ ] | DatePicker, TimePicker, Picker demos |
| ListsPage.xaml | [ ] | CollectionView demos |
| ProgressPage.xaml | [ ] | ProgressBar, ActivityIndicator demos |
| GridsPage.xaml | [ ] | Grid layout demos |
| AboutPage.xaml | [ ] | About information |
| DetailPage.xaml | [ ] | Navigation detail page |
### TodoApp (app + 3 pages)
| File | Status | Notes |
|------|--------|-------|
| App.xaml | [ ] | Colors, Icon strings |
| TodoListPage.xaml | [ ] | Main list with swipe actions |
| NewTodoPage.xaml | [ ] | Add new todo form |
| TodoDetailPage.xaml | [ ] | Edit todo details |
### XamlBrowser (app + 1 page) - COMPLETE
| File | Status | Notes |
|------|--------|-------|
| App.xaml | [x] | Colors, styles (NavButtonStyle, GoButtonStyle, AddressBarStyle, StatusLabelStyle) |
| App.xaml.cs | [x] | BrowserApp with ToggleTheme() |
| MainPage.xaml | [x] | Toolbar with nav buttons, address bar, WebView, status bar |
| MainPage.xaml.cs | [x] | Navigation logic, progress animation, theme toggle |
| MauiProgram.cs | [x] | UseLinuxPlatform() setup |
| Program.cs | [x] | LinuxProgramHost entry point |
| Resources/Images/*.svg | [x] | 10 toolbar icons (dark/light variants) |
---
## How to Reconstruct XAML
### Step 1: Read the decompiled InitializeComponent()
Look for patterns like:
```csharp
// Setting a property
((BindableObject)val8).SetValue(Label.TextProperty, (object)"OpenMaui");
// AppThemeBinding (light/dark mode)
val7.Light = "White";
val7.Dark = "#E0E0E0";
// StaticResource
val.Key = "PrimaryColor";
// Layout hierarchy
((Layout)val12).Children.Add((IView)(object)val6);
```
### Step 2: Convert to XAML
```csharp
// This C#:
((BindableObject)val8).SetValue(Label.TextProperty, (object)"OpenMaui");
((BindableObject)val8).SetValue(Label.FontSizeProperty, (object)22.0);
((BindableObject)val8).SetValue(Label.FontAttributesProperty, (object)(FontAttributes)1);
val7.Light = "White";
val7.Dark = "#E0E0E0";
((BindableObject)val8).SetBinding(Label.TextColorProperty, val74);
```
```xml
<!-- Becomes this XAML: -->
<Label Text="OpenMaui"
FontSize="22"
FontAttributes="Bold"
TextColor="{AppThemeBinding Light=White, Dark=#E0E0E0}" />
```
### Step 3: Verify with screenshots (when provided)
Compare the reconstructed XAML against the actual screenshots to ensure visual fidelity.
---
## App.xaml Resources Reference
### ShellDemo Colors (extracted from decompiled)
```xml
<!-- Light theme -->
<Color x:Key="PrimaryColor">#2196F3</Color>
<Color x:Key="PrimaryDarkColor">#1976D2</Color>
<Color x:Key="AccentColor">#FF4081</Color>
<Color x:Key="PageBackgroundLight">#F8F8F8</Color>
<Color x:Key="CardBackgroundLight">#FFFFFF</Color>
<Color x:Key="TextPrimaryLight">#212121</Color>
<Color x:Key="TextSecondaryLight">#757575</Color>
<Color x:Key="BorderLight">#E0E0E0</Color>
<Color x:Key="EntryBackgroundLight">#F9F9F9</Color>
<Color x:Key="ShellBackgroundLight">#FFFFFF</Color>
<Color x:Key="FlyoutBackgroundLight">#FFFFFF</Color>
<Color x:Key="ProgressTrackLight">#E0E0E0</Color>
<!-- Dark theme -->
<Color x:Key="PageBackgroundDark">#121212</Color>
<Color x:Key="CardBackgroundDark">#1E1E1E</Color>
<Color x:Key="TextPrimaryDark">#FFFFFF</Color>
<Color x:Key="TextSecondaryDark">#B0B0B0</Color>
<Color x:Key="BorderDark">#424242</Color>
<Color x:Key="EntryBackgroundDark">#2C2C2C</Color>
<Color x:Key="ShellBackgroundDark">#1E1E1E</Color>
<Color x:Key="FlyoutBackgroundDark">#1E1E1E</Color>
<Color x:Key="ProgressTrackDark">#424242</Color>
```
### ShellDemo Styles (extracted from decompiled)
- **ThemedEntry**: BackgroundColor, TextColor, PlaceholderColor with AppThemeBinding
- **ThemedEditor**: BackgroundColor, TextColor, PlaceholderColor with AppThemeBinding
- **TitleLabel**: FontSize=24, FontAttributes=Bold, TextColor with AppThemeBinding
- **SubtitleLabel**: FontSize=16, TextColor with AppThemeBinding
- **ThemedFrame**: BackgroundColor, BorderColor with AppThemeBinding
- **ThemedProgressBar**: ProgressColor=PrimaryColor, BackgroundColor with AppThemeBinding
- **PrimaryButton**: BackgroundColor=PrimaryColor, TextColor=White
- **SecondaryButton**: Light/dark themed background and text
### TodoApp Colors
```xml
<Color x:Key="PrimaryColor">#5C6BC0</Color>
<Color x:Key="PrimaryDarkColor">#3949AB</Color>
<Color x:Key="AccentColor">#26A69A</Color>
<Color x:Key="DangerColor">#EF5350</Color>
<!-- ... plus light/dark theme colors -->
```
### TodoApp Icons (Material Design)
```xml
<x:String x:Key="IconAdd">&#xe145;</x:String>
<x:String x:Key="IconDelete">&#xe872;</x:String>
<x:String x:Key="IconSave">&#xe161;</x:String>
<x:String x:Key="IconCheck">&#xe876;</x:String>
<x:String x:Key="IconEdit">&#xe3c9;</x:String>
```
---
## AppShell.xaml Structure (ShellDemo)
From decompiled code, the shell has:
```xml
<Shell Title="OpenMaui Controls Demo"
FlyoutBehavior="Flyout"
FlyoutBackgroundColor="{AppThemeBinding Light={StaticResource FlyoutBackgroundLight}, Dark={StaticResource FlyoutBackgroundDark}}">
<!-- FlyoutHeader: Grid with logo and title -->
<Shell.FlyoutHeader>
<Grid BackgroundColor="{AppThemeBinding ...}" HeightRequest="140" Padding="15">
<HorizontalStackLayout VerticalOptions="Center" Spacing="12">
<Image Source="openmaui_logo.svg" WidthRequest="60" HeightRequest="60" />
<VerticalStackLayout VerticalOptions="Center">
<Label Text="OpenMaui" FontSize="22" FontAttributes="Bold"
TextColor="{AppThemeBinding Light=White, Dark=#E0E0E0}" />
<Label Text="Controls Demo" FontSize="13" Opacity="0.9"
TextColor="{AppThemeBinding Light=White, Dark=#B0B0B0}" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Grid>
</Shell.FlyoutHeader>
<!-- FlyoutItems with emoji icons -->
<FlyoutItem Title="Home" Route="Home">
<FlyoutItem.Icon><FontImageSource Glyph="🏠" FontFamily="Default" Color="{AppThemeBinding ...}" /></FlyoutItem.Icon>
<ShellContent ContentTemplate="{DataTemplate local:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="Buttons" Route="Buttons">...</FlyoutItem>
<FlyoutItem Title="Text Input" Route="TextInput">...</FlyoutItem>
<FlyoutItem Title="Selection" Route="Selection">...</FlyoutItem>
<FlyoutItem Title="Pickers" Route="Pickers">...</FlyoutItem>
<FlyoutItem Title="Lists" Route="Lists">...</FlyoutItem>
<FlyoutItem Title="Progress" Route="Progress">...</FlyoutItem>
<FlyoutItem Title="Grids" Route="Grids">...</FlyoutItem>
<FlyoutItem Title="About" Route="About">...</FlyoutItem>
</Shell>
```
---
## Decompiled File Locations
| Sample | Decompiled Path |
|--------|-----------------|
| ShellDemo | `/Users/nible/Documents/GitHub/recovered/source/ShellDemo/ShellDemo/` |
| TodoApp | `/Users/nible/Documents/GitHub/recovered/source/TodoApp/TodoApp/` |
| XamlBrowser | `/Users/nible/Documents/GitHub/recovered/source/XamlBrowser/XamlBrowser/` |
---
## Build Command
```bash
cd /Users/nible/Documents/GitHub/maui-linux-main
dotnet build OpenMaui.Controls.Linux.csproj
```
---
## Key Patterns in Decompiled Code
### 1. Color Values
```csharp
Color val = new Color(11f / 85f, 0.5882353f, 81f / 85f, 1f);
// = Color.FromRgba(0.129, 0.588, 0.953, 1.0) = #2196F3
```
### 2. AppThemeBinding
```csharp
AppThemeBindingExtension val7 = new AppThemeBindingExtension();
val7.Light = "White";
val7.Dark = "#E0E0E0";
```
Becomes: `{AppThemeBinding Light=White, Dark=#E0E0E0}`
### 3. StaticResource
```csharp
val.Key = "PrimaryColor";
```
Becomes: `{StaticResource PrimaryColor}`
### 4. Layout Hierarchy
```csharp
((Layout)val12).Children.Add((IView)(object)val6);
((Layout)val12).Children.Add((IView)(object)val11);
```
The children are added in order - first child is val6, second is val11.
### 5. FontAttributes Enum
```csharp
(FontAttributes)1 // Bold
(FontAttributes)2 // Italic
```
---
## Workflow for Each File
1. **Read decompiled** `InitializeComponent()` method
2. **Extract** all UI elements and their properties
3. **Write XAML** with proper structure
4. **Create code-behind** (usually just constructor calling InitializeComponent)
5. **Verify** against screenshot if available
6. **Update tracking** in this file
7. **Commit** with descriptive message
---
## Notes
- The decompiled code has ALL the information needed - it's just in C# form instead of XAML
- Screenshots will help verify visual accuracy
- Focus on one file at a time
- Commit after each completed file

View File

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

@@ -235,25 +235,3 @@ public class SKColorTypeConverter : TypeConverter
}
}
}
/// <summary>
/// Extension methods for color conversion.
/// </summary>
public static class ColorExtensions
{
/// <summary>
/// Converts a MAUI Color to an SKColor.
/// </summary>
public static SKColor ToSKColor(this Color color)
{
return SKColorTypeConverter.ToSKColor(color);
}
/// <summary>
/// Converts an SKColor to a MAUI Color.
/// </summary>
public static Color ToMauiColor(this SKColor color)
{
return SKColorTypeConverter.ToMauiColor(color);
}
}

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

@@ -137,192 +137,3 @@ public class SKRectTypeConverter : TypeConverter
return SKRect.Empty;
}
}
/// <summary>
/// Type converter for SKSize.
/// </summary>
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 uniform))
{
return new SKSize(uniform, uniform);
}
}
else if (parts.Length == 2)
{
if (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;
}
}
/// <summary>
/// Type converter for SKPoint.
/// </summary>
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)
{
if (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;
}
}
/// <summary>
/// Extension methods for SkiaSharp type conversions.
/// </summary>
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,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,76 @@
using System;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform.Linux.Native;
namespace Microsoft.Maui.Platform.Linux.Dispatching;
public class LinuxDispatcher : IDispatcher
{
private static int _mainThreadId;
private static LinuxDispatcher? _mainDispatcher;
private static readonly object _lock = new object();
public static LinuxDispatcher? Main => _mainDispatcher;
public static bool IsMainThread => Environment.CurrentManagedThreadId == _mainThreadId;
public bool IsDispatchRequired => !IsMainThread;
public static void Initialize()
{
lock (_lock)
{
_mainThreadId = Environment.CurrentManagedThreadId;
_mainDispatcher = new LinuxDispatcher();
Console.WriteLine($"[LinuxDispatcher] Initialized on thread {_mainThreadId}");
}
}
public bool Dispatch(Action action)
{
ArgumentNullException.ThrowIfNull(action, "action");
if (!IsDispatchRequired)
{
action();
return true;
}
GLibNative.IdleAdd(delegate
{
try
{
action();
}
catch (Exception ex)
{
Console.WriteLine("[LinuxDispatcher] Error in dispatched action: " + ex.Message);
}
return false;
});
return true;
}
public bool DispatchDelayed(TimeSpan delay, Action action)
{
ArgumentNullException.ThrowIfNull(action, "action");
GLibNative.TimeoutAdd((uint)Math.Max(0.0, delay.TotalMilliseconds), delegate
{
try
{
action();
}
catch (Exception ex)
{
Console.WriteLine("[LinuxDispatcher] Error in delayed action: " + ex.Message);
}
return false;
});
return true;
}
public IDispatcherTimer CreateTimer()
{
return new LinuxDispatcherTimer(this);
}
}

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

11
DisplayServerType.cs Normal file
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,63 +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,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
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();
}
public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
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

@@ -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.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -20,10 +21,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 =
@@ -48,13 +56,49 @@ 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 || handler.MauiContext is null) return;
@@ -68,7 +112,7 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
if (content.Handler == null)
{
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToHandler(handler.MauiContext);
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)
@@ -85,14 +129,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)
@@ -101,7 +145,7 @@ 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;
}
}
@@ -109,22 +153,27 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.BackgroundColor != null)
if (border is VisualElement ve)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
var bgColor = ve.BackgroundColor;
Console.WriteLine($"[BorderHandler] MapBackgroundColor: {bgColor}");
if (bgColor != null)
{
handler.PlatformView.BackgroundColor = bgColor;
handler.PlatformView.Invalidate();
}
}
}
public static void MapPadding(BorderHandler handler, IBorderView border)
{
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)
@@ -135,24 +184,109 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
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)
{
// RoundRectangle can have different corner radii, but we use a uniform one
// Take the top-left corner as the uniform radius
var cornerRadius = roundRect.CornerRadius;
handler.PlatformView.CornerRadius = (float)cornerRadius.TopLeft;
handler.PlatformView.CornerRadius = cornerRadius.TopLeft;
}
else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle)
{
handler.PlatformView.CornerRadius = 0;
handler.PlatformView.CornerRadius = 0.0;
}
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
{
// For ellipse, use half the min dimension as corner radius
// This will be applied during rendering when bounds are known
handler.PlatformView.CornerRadius = float.MaxValue; // Marker for "fully rounded"
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

@@ -30,28 +30,38 @@ public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
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 = new SKColor(
(byte)(boxView.Color.Red * 255),
(byte)(boxView.Color.Green * 255),
(byte)(boxView.Color.Blue * 255),
(byte)(boxView.Color.Alpha * 255));
handler.PlatformView.Color = boxView.Color;
}
}
public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView)
{
handler.PlatformView.CornerRadius = (float)boxView.CornerRadius.TopLeft;
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.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
handler.PlatformView.Invalidate();
}
}
@@ -60,7 +70,7 @@ public partial class BoxViewHandler : ViewHandler<BoxView, SkiaBoxView>
{
if (boxView.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = boxView.BackgroundColor.ToSKColor();
handler.PlatformView.BackgroundColor = boxView.BackgroundColor;
handler.PlatformView.Invalidate();
}
}

View File

@@ -1,179 +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;
// Manually map all properties on connect since MAUI may not trigger updates
// for properties that were set before handler connection
if (VirtualView != null)
{
MapText(this, VirtualView);
MapTextColor(this, VirtualView);
MapBackground(this, VirtualView);
MapFont(this, VirtualView);
MapPadding(this, VirtualView);
MapCornerRadius(this, VirtualView);
MapBorderColor(this, VirtualView);
MapBorderWidth(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
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)
{
// Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = 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)
{
Console.WriteLine($"[ButtonHandler] MapIsEnabled called - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using SkiaSharp;
@@ -59,6 +60,20 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
MapBackground(this, VirtualView);
MapPadding(this, VirtualView);
MapIsEnabled(this, VirtualView);
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
Console.WriteLine($"[ButtonHandler] MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
else
{
Console.WriteLine($"[ButtonHandler] VirtualView is NOT Microsoft.Maui.Controls.Button, type={VirtualView?.GetType().Name}");
}
}
}
@@ -80,13 +95,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)
@@ -101,8 +116,8 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
// Set ButtonBackgroundColor (used for rendering) not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor();
// Set BackgroundColor (MAUI Color type)
handler.PlatformView.BackgroundColor = solidPaint.Color;
}
}
@@ -111,17 +126,16 @@ 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;
Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}
@@ -148,6 +162,7 @@ public partial class TextButtonHandler : ButtonHandler
protected override void ConnectHandler(SkiaButton platformView)
{
Console.WriteLine($"[TextButtonHandler] ConnectHandler START");
base.ConnectHandler(platformView);
// Manually map text properties on connect since MAUI may not trigger updates
@@ -159,6 +174,17 @@ public partial class TextButtonHandler : ButtonHandler
MapFont(this, textButton);
MapCharacterSpacing(this, textButton);
}
// Map size requests from MAUI Button
if (VirtualView is Microsoft.Maui.Controls.Button mauiButton)
{
Console.WriteLine($"[TextButtonHandler] MapSize Text='{platformView.Text}' WReq={mauiButton.WidthRequest} HReq={mauiButton.HeightRequest}");
if (mauiButton.WidthRequest >= 0)
platformView.WidthRequest = mauiButton.WidthRequest;
if (mauiButton.HeightRequest >= 0)
platformView.HeightRequest = mauiButton.HeightRequest;
}
Console.WriteLine($"[TextButtonHandler] ConnectHandler DONE");
}
public static void MapText(TextButtonHandler handler, ITextButton button)
@@ -172,7 +198,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)
@@ -181,18 +207,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,113 +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,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <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();
}
public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox)
{
if (checkBox.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(CheckBoxHandler handler, ICheckBox checkBox)
{
if (checkBox is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -18,6 +18,7 @@ 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,
};
@@ -72,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;
}
}
@@ -82,10 +83,16 @@ 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;

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.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -123,7 +124,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;
Console.WriteLine($"[CollectionViewHandler] OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}");
// Try to get the item view and process gestures
var skiaView = PlatformView?.GetItemView(e.Index);
Console.WriteLine($"[CollectionViewHandler] GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? "null"}, MauiView={skiaView?.MauiView?.GetType().Name ?? "null"}");
if (skiaView?.MauiView != null)
{
Console.WriteLine($"[CollectionViewHandler] Found MauiView: {skiaView.MauiView.GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}");
if (GestureManager.ProcessTap(skiaView.MauiView, 0, 0))
{
Console.WriteLine("[CollectionViewHandler] Gesture processed successfully");
return;
}
}
// Handle selection if gesture wasn't processed
if (VirtualView.SelectionMode == SelectionMode.Single)
{
VirtualView.SelectedItem = e.Item;
}
else if (VirtualView.SelectionMode == SelectionMode.Multiple)
{
if (VirtualView.SelectedItems.Contains(e.Item))
{
VirtualView.SelectedItems.Remove(e.Item);
}
else
{
VirtualView.SelectedItems.Add(e.Item);
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView)
@@ -158,11 +201,14 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
// Create handler for the view
if (view.Handler == null && handler.MauiContext != null)
{
view.Handler = view.ToHandler(handler.MauiContext);
view.Handler = view.ToViewHandler(handler.MauiContext);
}
if (view.Handler?.PlatformView is SkiaView skiaView)
{
// Set MauiView so gestures can be processed
skiaView.MauiView = view;
Console.WriteLine($"[CollectionViewHandler.ItemViewCreator] Set MauiView={view.GetType().Name} on {skiaView.GetType().Name}, GestureRecognizers={view.GestureRecognizers?.Count ?? 0}");
return skiaView;
}
}
@@ -174,7 +220,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
{
if (cellView.Handler == null && handler.MauiContext != null)
{
cellView.Handler = cellView.ToHandler(handler.MauiContext);
cellView.Handler = cellView.ToViewHandler(handler.MauiContext);
}
if (cellView.Handler?.PlatformView is SkiaView skiaView)
@@ -315,7 +361,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
if (collectionView.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
@@ -325,7 +371,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
if (collectionView.BackgroundColor is not null)
{
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor.ToSKColor();
handler.PlatformView.BackgroundColor = collectionView.BackgroundColor;
}
}

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,
@@ -97,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;
}
}
@@ -106,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)
@@ -123,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)
@@ -140,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)
@@ -164,7 +211,7 @@ 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;
}
}
@@ -172,9 +219,9 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
{
if (handler.PlatformView is null) return;
if (editor is VisualElement ve && ve.BackgroundColor != null)
if (editor is Editor ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.EditorBackgroundColor = ve.BackgroundColor;
handler.PlatformView.Invalidate();
}
}

View File

@@ -1,199 +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,
["BackgroundColor"] = MapBackgroundColor,
};
/// <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();
}
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
{
if (entry is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.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)
@@ -96,7 +101,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)
@@ -105,19 +110,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)
@@ -131,7 +141,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)
@@ -177,16 +187,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
};
}
@@ -196,10 +218,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
};
}
@@ -209,7 +231,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,14 +62,83 @@ public partial class FlyoutPageHandler : ViewHandler<IFlyoutView, SkiaFlyoutPage
private void OnIsPresentedChanged(object? sender, EventArgs e)
{
if (VirtualView is null || PlatformView is null || _isUpdatingPresented) return;
try
{
_isUpdatingPresented = true;
// Sync back to the virtual view
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;
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);
}
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -37,15 +38,36 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
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 = new SKColor(
(byte)(frame.BorderColor.Red * 255),
(byte)(frame.BorderColor.Green * 255),
(byte)(frame.BorderColor.Blue * 255),
(byte)(frame.BorderColor.Alpha * 255));
handler.PlatformView.Stroke = frame.BorderColor;
}
}
@@ -63,11 +85,7 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
{
if (frame.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = new SKColor(
(byte)(frame.BackgroundColor.Red * 255),
(byte)(frame.BackgroundColor.Green * 255),
(byte)(frame.BackgroundColor.Blue * 255),
(byte)(frame.BackgroundColor.Alpha * 255));
handler.PlatformView.BackgroundColor = frame.BackgroundColor;
}
}
@@ -92,7 +110,7 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToHandler(handler.MauiContext);
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)

845
Handlers/GestureManager.cs Normal file
View File

@@ -0,0 +1,845 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows.Input;
using Microsoft.Maui.Controls;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Manages gesture recognition and processing for MAUI views on Linux.
/// Handles tap, pan, swipe, pinch, and pointer gestures.
/// </summary>
public static class GestureManager
{
private class GestureTrackingState
{
public double StartX { get; set; }
public double StartY { get; set; }
public double CurrentX { get; set; }
public double CurrentY { get; set; }
public DateTime StartTime { get; set; }
public bool IsPanning { get; set; }
public bool IsPressed { get; set; }
public bool IsPinching { get; set; }
public double PinchScale { get; set; } = 1.0;
}
private enum PointerEventType
{
Entered,
Exited,
Pressed,
Moved,
Released
}
private static MethodInfo? _sendTappedMethod;
private static MethodInfo? _sendPinchMethod;
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new Dictionary<View, (DateTime, int)>();
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new Dictionary<View, GestureTrackingState>();
private const double SwipeMinDistance = 50.0;
private const double SwipeMaxTime = 500.0;
private const double SwipeDirectionThreshold = 0.5;
private const double PanMinDistance = 10.0;
private const double PinchScrollScale = 0.1; // Scale factor per scroll unit
/// <summary>
/// Processes a tap gesture on the specified view.
/// </summary>
public static bool ProcessTap(View? view, double x, double y)
{
if (view == null)
{
return false;
}
var current = view;
while (current != null)
{
var recognizers = current.GestureRecognizers;
if (recognizers != null && recognizers.Count > 0 && ProcessTapOnView(current, x, y))
{
return true;
}
var parent = current.Parent;
current = (parent is View parentView) ? parentView : null;
}
return false;
}
private static bool ProcessTapOnView(View view, double x, double y)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null || recognizers.Count == 0)
{
return false;
}
bool result = false;
foreach (var item in recognizers)
{
var tapRecognizer = (item is TapGestureRecognizer) ? (TapGestureRecognizer)item : null;
if (tapRecognizer == null)
{
continue;
}
Console.WriteLine($"[GestureManager] Processing TapGestureRecognizer on {view.GetType().Name}, CommandParameter={tapRecognizer.CommandParameter}, NumberOfTapsRequired={tapRecognizer.NumberOfTapsRequired}");
int numberOfTapsRequired = tapRecognizer.NumberOfTapsRequired;
if (numberOfTapsRequired > 1)
{
DateTime utcNow = DateTime.UtcNow;
if (!_tapTracking.TryGetValue(view, out var tracking))
{
_tapTracking[view] = (utcNow, 1);
Console.WriteLine($"[GestureManager] First tap 1/{numberOfTapsRequired}");
continue;
}
if (!((utcNow - tracking.lastTap).TotalMilliseconds < 300.0))
{
_tapTracking[view] = (utcNow, 1);
Console.WriteLine($"[GestureManager] Tap timeout, reset to 1/{numberOfTapsRequired}");
continue;
}
int tapCount = tracking.tapCount + 1;
if (tapCount < numberOfTapsRequired)
{
_tapTracking[view] = (utcNow, tapCount);
Console.WriteLine($"[GestureManager] Tap {tapCount}/{numberOfTapsRequired}, waiting for more taps");
continue;
}
_tapTracking.Remove(view);
}
bool eventFired = false;
try
{
if (_sendTappedMethod == null)
{
_sendTappedMethod = typeof(TapGestureRecognizer).GetMethod("SendTapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendTappedMethod != null)
{
Console.WriteLine($"[GestureManager] Found SendTapped method with {_sendTappedMethod.GetParameters().Length} params");
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
_sendTappedMethod.Invoke(tapRecognizer, new object[] { view, args });
Console.WriteLine("[GestureManager] SendTapped invoked successfully");
eventFired = true;
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] SendTapped failed: " + ex.Message);
}
if (!eventFired)
{
try
{
var field = typeof(TapGestureRecognizer).GetField("Tapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?? typeof(TapGestureRecognizer).GetField("_tapped", BindingFlags.Instance | BindingFlags.NonPublic);
if (field != null && field.GetValue(tapRecognizer) is EventHandler<TappedEventArgs> handler)
{
Console.WriteLine("[GestureManager] Invoking Tapped event directly");
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
handler(tapRecognizer, args);
eventFired = true;
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] Direct event invoke failed: " + ex.Message);
}
}
if (!eventFired)
{
try
{
string[] fieldNames = new string[] { "TappedEvent", "_TappedHandler", "<Tapped>k__BackingField" };
foreach (string fieldName in fieldNames)
{
var field = typeof(TapGestureRecognizer).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field != null)
{
Console.WriteLine("[GestureManager] Found field: " + fieldName);
if (field.GetValue(tapRecognizer) is EventHandler<TappedEventArgs> handler)
{
var args = new TappedEventArgs(tapRecognizer.CommandParameter);
handler(tapRecognizer, args);
Console.WriteLine("[GestureManager] Event fired via " + fieldName);
eventFired = true;
break;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] Backing field approach failed: " + ex.Message);
}
}
if (!eventFired)
{
Console.WriteLine("[GestureManager] Could not fire event, dumping type info...");
var methods = typeof(TapGestureRecognizer).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var method in methods)
{
if (method.Name.Contains("Tap", StringComparison.OrdinalIgnoreCase) || method.Name.Contains("Send", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"[GestureManager] Method: {method.Name}({string.Join(", ", from p in method.GetParameters() select p.ParameterType.Name)})");
}
}
}
ICommand? command = tapRecognizer.Command;
if (command != null && command.CanExecute(tapRecognizer.CommandParameter))
{
Console.WriteLine("[GestureManager] Executing Command");
command.Execute(tapRecognizer.CommandParameter);
}
result = true;
}
return result;
}
/// <summary>
/// Checks if the view has any gesture recognizers.
/// </summary>
public static bool HasGestureRecognizers(View? view)
{
if (view == null)
{
return false;
}
return view.GestureRecognizers?.Count > 0;
}
/// <summary>
/// Checks if the view has a tap gesture recognizer.
/// </summary>
public static bool HasTapGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is TapGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Processes a pointer down event.
/// </summary>
public static void ProcessPointerDown(View? view, double x, double y)
{
if (view != null)
{
_gestureState[view] = new GestureTrackingState
{
StartX = x,
StartY = y,
CurrentX = x,
CurrentY = y,
StartTime = DateTime.UtcNow,
IsPanning = false,
IsPressed = true
};
ProcessPointerEvent(view, x, y, PointerEventType.Pressed);
}
}
/// <summary>
/// Processes a pointer move event.
/// </summary>
public static void ProcessPointerMove(View? view, double x, double y)
{
if (view == null)
{
return;
}
if (!_gestureState.TryGetValue(view, out var state))
{
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
return;
}
state.CurrentX = x;
state.CurrentY = y;
if (!state.IsPressed)
{
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
return;
}
double deltaX = x - state.StartX;
double deltaY = y - state.StartY;
if (Math.Sqrt(deltaX * deltaX + deltaY * deltaY) >= 10.0)
{
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)(state.IsPanning ? 1 : 0));
state.IsPanning = true;
}
ProcessPointerEvent(view, x, y, PointerEventType.Moved);
}
/// <summary>
/// Processes a pointer up event.
/// </summary>
public static void ProcessPointerUp(View? view, double x, double y)
{
if (view == null)
{
return;
}
if (_gestureState.TryGetValue(view, out var state))
{
state.CurrentX = x;
state.CurrentY = y;
double deltaX = x - state.StartX;
double deltaY = y - state.StartY;
double distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
double elapsed = (DateTime.UtcNow - state.StartTime).TotalMilliseconds;
if (distance >= 50.0 && elapsed <= 500.0)
{
var direction = DetermineSwipeDirection(deltaX, deltaY);
if (direction != SwipeDirection.Right)
{
ProcessSwipeGesture(view, direction);
}
else if (Math.Abs(deltaX) > Math.Abs(deltaY) * 0.5)
{
ProcessSwipeGesture(view, (deltaX > 0.0) ? SwipeDirection.Right : SwipeDirection.Left);
}
}
if (state.IsPanning)
{
ProcessPanGesture(view, deltaX, deltaY, (GestureStatus)2);
}
else if (distance < 15.0 && elapsed < 500.0)
{
Console.WriteLine($"[GestureManager] Detected tap on {view.GetType().Name} (distance={distance:F1}, elapsed={elapsed:F0}ms)");
ProcessTap(view, x, y);
}
_gestureState.Remove(view);
}
ProcessPointerEvent(view, x, y, PointerEventType.Released);
}
/// <summary>
/// Processes a pointer entered event.
/// </summary>
public static void ProcessPointerEntered(View? view, double x, double y)
{
if (view != null)
{
ProcessPointerEvent(view, x, y, PointerEventType.Entered);
}
}
/// <summary>
/// Processes a pointer exited event.
/// </summary>
public static void ProcessPointerExited(View? view, double x, double y)
{
if (view != null)
{
ProcessPointerEvent(view, x, y, PointerEventType.Exited);
}
}
private static SwipeDirection DetermineSwipeDirection(double deltaX, double deltaY)
{
double absX = Math.Abs(deltaX);
double absY = Math.Abs(deltaY);
if (absX > absY * 0.5)
{
if (deltaX > 0.0)
{
return SwipeDirection.Right;
}
return SwipeDirection.Left;
}
if (absY > absX * 0.5)
{
if (deltaY > 0.0)
{
return SwipeDirection.Down;
}
return SwipeDirection.Up;
}
if (deltaX > 0.0)
{
return SwipeDirection.Right;
}
return SwipeDirection.Left;
}
private static void ProcessSwipeGesture(View view, SwipeDirection direction)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
var swipeRecognizer = (item is SwipeGestureRecognizer) ? (SwipeGestureRecognizer)item : null;
if (swipeRecognizer == null || !swipeRecognizer.Direction.HasFlag(direction))
{
continue;
}
Console.WriteLine($"[GestureManager] Swipe detected: {direction}");
try
{
var method = typeof(SwipeGestureRecognizer).GetMethod("SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(swipeRecognizer, new object[] { view, direction });
Console.WriteLine("[GestureManager] SendSwiped invoked successfully");
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] SendSwiped failed: " + ex.Message);
}
ICommand? command = swipeRecognizer.Command;
if (command != null && command.CanExecute(swipeRecognizer.CommandParameter))
{
swipeRecognizer.Command.Execute(swipeRecognizer.CommandParameter);
}
}
}
private static void ProcessPanGesture(View view, double totalX, double totalY, GestureStatus status)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
var panRecognizer = (item is PanGestureRecognizer) ? (PanGestureRecognizer)item : null;
if (panRecognizer == null)
{
continue;
}
Console.WriteLine($"[GestureManager] Pan gesture: status={status}, totalX={totalX:F1}, totalY={totalY:F1}");
try
{
var method = typeof(PanGestureRecognizer).GetMethod("SendPan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(panRecognizer, new object[]
{
view,
totalX,
totalY,
(int)status
});
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] SendPan failed: " + ex.Message);
}
}
}
private static void ProcessPointerEvent(View view, double x, double y, PointerEventType eventType)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
var pointerRecognizer = (item is PointerGestureRecognizer) ? (PointerGestureRecognizer)item : null;
if (pointerRecognizer == null)
{
continue;
}
try
{
string? methodName = eventType switch
{
PointerEventType.Entered => "SendPointerEntered",
PointerEventType.Exited => "SendPointerExited",
PointerEventType.Pressed => "SendPointerPressed",
PointerEventType.Moved => "SendPointerMoved",
PointerEventType.Released => "SendPointerReleased",
_ => null,
};
if (methodName != null)
{
var method = typeof(PointerGestureRecognizer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
var args = CreatePointerEventArgs(view, x, y);
method.Invoke(pointerRecognizer, new object[] { view, args });
}
}
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] Pointer event failed: " + ex.Message);
}
}
}
private static object CreatePointerEventArgs(View view, double x, double y)
{
try
{
var type = typeof(PointerGestureRecognizer).Assembly.GetType("Microsoft.Maui.Controls.PointerEventArgs");
if (type != null)
{
var ctor = type.GetConstructors().FirstOrDefault();
if (ctor != null)
{
return ctor.Invoke(new object[0]);
}
}
}
catch
{
}
return null!;
}
/// <summary>
/// Processes a scroll event that may be a pinch gesture (Ctrl+Scroll).
/// Returns true if the scroll was consumed as a pinch gesture.
/// </summary>
public static bool ProcessScrollAsPinch(View? view, double x, double y, double deltaY, bool isCtrlPressed)
{
if (view == null || !isCtrlPressed)
{
return false;
}
// Check if view has a pinch gesture recognizer
if (!HasPinchGestureRecognizer(view))
{
return false;
}
// Get or create gesture state
if (!_gestureState.TryGetValue(view, out var state))
{
state = new GestureTrackingState
{
StartX = x,
StartY = y,
CurrentX = x,
CurrentY = y,
StartTime = DateTime.UtcNow,
PinchScale = 1.0
};
_gestureState[view] = state;
}
// Calculate new scale based on scroll delta
double scaleDelta = 1.0 + (deltaY * PinchScrollScale);
state.PinchScale *= scaleDelta;
// Clamp scale to reasonable bounds
state.PinchScale = Math.Clamp(state.PinchScale, 0.1, 10.0);
GestureStatus status;
if (!state.IsPinching)
{
state.IsPinching = true;
status = GestureStatus.Started;
}
else
{
status = GestureStatus.Running;
}
ProcessPinchGesture(view, state.PinchScale, x, y, status);
return true;
}
/// <summary>
/// Ends an ongoing pinch gesture.
/// </summary>
public static void EndPinchGesture(View? view)
{
if (view == null) return;
if (_gestureState.TryGetValue(view, out var state) && state.IsPinching)
{
ProcessPinchGesture(view, state.PinchScale, state.CurrentX, state.CurrentY, GestureStatus.Completed);
state.IsPinching = false;
state.PinchScale = 1.0;
}
}
private static void ProcessPinchGesture(View view, double scale, double originX, double originY, GestureStatus status)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null)
{
return;
}
foreach (var item in recognizers)
{
var pinchRecognizer = item as PinchGestureRecognizer;
if (pinchRecognizer == null)
{
continue;
}
Console.WriteLine($"[GestureManager] Pinch gesture: status={status}, scale={scale:F2}, origin=({originX:F0},{originY:F0})");
try
{
// Cache the method lookup
if (_sendPinchMethod == null)
{
_sendPinchMethod = typeof(PinchGestureRecognizer).GetMethod("SendPinch",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_sendPinchMethod != null)
{
// SendPinch(IView sender, double scale, Point scaleOrigin, GestureStatus status)
var scaleOrigin = new Point(originX / view.Width, originY / view.Height);
_sendPinchMethod.Invoke(pinchRecognizer, new object[]
{
view,
scale,
scaleOrigin,
status
});
Console.WriteLine("[GestureManager] SendPinch invoked successfully");
}
}
catch (Exception ex)
{
Console.WriteLine($"[GestureManager] SendPinch failed: {ex.Message}");
}
}
}
/// <summary>
/// Checks if the view has a pinch gesture recognizer.
/// </summary>
public static bool HasPinchGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PinchGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a swipe gesture recognizer.
/// </summary>
public static bool HasSwipeGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is SwipeGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a pan gesture recognizer.
/// </summary>
public static bool HasPanGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PanGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a pointer gesture recognizer.
/// </summary>
public static bool HasPointerGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is PointerGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a drag gesture recognizer.
/// </summary>
public static bool HasDragGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is DragGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if the view has a drop gesture recognizer.
/// </summary>
public static bool HasDropGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null)
{
return false;
}
foreach (var recognizer in view.GestureRecognizers)
{
if (recognizer is DropGestureRecognizer)
{
return true;
}
}
return false;
}
/// <summary>
/// Initiates a drag operation from the specified view.
/// </summary>
public static void StartDrag(View? view, double x, double y)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
var dragRecognizer = item as DragGestureRecognizer;
if (dragRecognizer == null) continue;
Console.WriteLine($"[GestureManager] Starting drag from {view.GetType().Name}");
try
{
// Create DragStartingEventArgs and invoke SendDragStarting
var method = typeof(DragGestureRecognizer).GetMethod("SendDragStarting",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(dragRecognizer, new object[] { view });
Console.WriteLine("[GestureManager] SendDragStarting invoked successfully");
}
}
catch (Exception ex)
{
Console.WriteLine($"[GestureManager] SendDragStarting failed: {ex.Message}");
}
}
}
/// <summary>
/// Processes a drag enter event on the specified view.
/// </summary>
public static void ProcessDragEnter(View? view, double x, double y, object? data)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
var dropRecognizer = item as DropGestureRecognizer;
if (dropRecognizer == null) continue;
Console.WriteLine($"[GestureManager] Drag enter on {view.GetType().Name}");
try
{
var method = typeof(DropGestureRecognizer).GetMethod("SendDragOver",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(dropRecognizer, new object[] { view });
}
}
catch (Exception ex)
{
Console.WriteLine($"[GestureManager] SendDragOver failed: {ex.Message}");
}
}
}
/// <summary>
/// Processes a drop event on the specified view.
/// </summary>
public static void ProcessDrop(View? view, double x, double y, object? data)
{
if (view == null) return;
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var item in recognizers)
{
var dropRecognizer = item as DropGestureRecognizer;
if (dropRecognizer == null) continue;
Console.WriteLine($"[GestureManager] Drop on {view.GetType().Name}");
try
{
var method = typeof(DropGestureRecognizer).GetMethod("SendDrop",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(dropRecognizer, new object[] { view });
}
}
catch (Exception ex)
{
Console.WriteLine($"[GestureManager] SendDrop failed: {ex.Message}");
}
}
}
}

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;
}
Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready");
}
protected override void DisconnectHandler(GtkWebViewProxy platformView)
{
if (_platformWebView != null)
{
_platformWebView.NavigationStarted -= OnNavigationStarted;
_platformWebView.NavigationCompleted -= OnNavigationCompleted;
_platformWebView.ScriptDialogRequested -= OnScriptDialogRequested;
UnregisterFromHost();
_platformWebView.Dispose();
_platformWebView = null;
}
base.DisconnectHandler(platformView);
}
private async void OnScriptDialogRequested(object? sender,
(ScriptDialogType Type, string Message, Action<bool> Callback) e)
{
Console.WriteLine($"[GtkWebViewHandler] Script dialog requested: type={e.Type}, message={e.Message}");
string title = e.Type switch
{
ScriptDialogType.Alert => "Alert",
ScriptDialogType.Confirm => "Confirm",
ScriptDialogType.Prompt => "Prompt",
_ => "Message"
};
string? acceptButton = e.Type == ScriptDialogType.Alert ? "OK" : "OK";
string? cancelButton = e.Type == ScriptDialogType.Alert ? null : "Cancel";
try
{
bool result = await LinuxDialogService.ShowAlertAsync(title, e.Message, acceptButton, cancelButton);
e.Callback(result);
Console.WriteLine($"[GtkWebViewHandler] Dialog result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"[GtkWebViewHandler] Error showing dialog: {ex.Message}");
e.Callback(false);
}
}
private void OnNavigationStarted(object? sender, string uri)
{
Console.WriteLine($"[GtkWebViewHandler] Navigation started: {uri}");
try
{
GLibNative.IdleAdd(() =>
{
try
{
if (VirtualView is IWebViewController controller)
{
var args = new Microsoft.Maui.Controls.WebNavigatingEventArgs(
WebNavigationEvent.NewPage, null, uri);
controller.SendNavigating(args);
Console.WriteLine("[GtkWebViewHandler] Sent Navigating event to VirtualView");
}
}
catch (Exception ex)
{
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigating: {ex.Message}");
}
return false;
});
}
catch (Exception ex)
{
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation started: {ex.Message}");
}
}
private void OnNavigationCompleted(object? sender, (string Url, bool Success) e)
{
Console.WriteLine($"[GtkWebViewHandler] Navigation completed: {e.Url} (Success: {e.Success})");
try
{
GLibNative.IdleAdd(() =>
{
try
{
if (VirtualView is IWebViewController controller)
{
var result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
var args = new Microsoft.Maui.Controls.WebNavigatedEventArgs(
WebNavigationEvent.NewPage, null, e.Url, result);
controller.SendNavigated(args);
bool canGoBack = _platformWebView?.CanGoBack() ?? false;
bool canGoForward = _platformWebView?.CanGoForward() ?? false;
controller.CanGoBack = canGoBack;
controller.CanGoForward = canGoForward;
Console.WriteLine($"[GtkWebViewHandler] Sent Navigated, CanGoBack={canGoBack}, CanGoForward={canGoForward}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[GtkWebViewHandler] Error in SendNavigated: {ex.Message}");
}
return false;
});
}
catch (Exception ex)
{
Console.WriteLine($"[GtkWebViewHandler] Error dispatching navigation completed: {ex.Message}");
}
}
internal void RegisterWithHost(SKRect bounds)
{
if (_platformWebView == null)
return;
var hostService = GtkHostService.Instance;
if (hostService.HostWindow == null || hostService.WebViewManager == null)
{
Console.WriteLine("[GtkWebViewHandler] Warning: GTK host not initialized, cannot register WebView");
return;
}
int x = (int)bounds.Left;
int y = (int)bounds.Top;
int width = (int)bounds.Width;
int height = (int)bounds.Height;
if (width <= 0 || height <= 0)
{
Console.WriteLine($"[GtkWebViewHandler] Skipping invalid bounds: {bounds}");
return;
}
if (!_isRegisteredWithHost)
{
hostService.HostWindow.AddWebView(_platformWebView.Widget, x, y, width, height);
_isRegisteredWithHost = true;
Console.WriteLine($"[GtkWebViewHandler] Registered WebView at ({x}, {y}) size {width}x{height}");
}
else if (bounds != _lastBounds)
{
hostService.HostWindow.MoveResizeWebView(_platformWebView.Widget, x, y, width, height);
Console.WriteLine($"[GtkWebViewHandler] Updated WebView to ({x}, {y}) size {width}x{height}");
}
_lastBounds = bounds;
}
private void UnregisterFromHost()
{
if (_isRegisteredWithHost && _platformWebView != null)
{
var hostService = GtkHostService.Instance;
if (hostService.HostWindow != null)
{
hostService.HostWindow.RemoveWebView(_platformWebView.Widget);
Console.WriteLine("[GtkWebViewHandler] Unregistered WebView from host");
}
_isRegisteredWithHost = false;
}
}
public static void MapSource(GtkWebViewHandler handler, IWebView webView)
{
if (handler._platformWebView == null)
return;
var source = webView.Source;
Console.WriteLine($"[GtkWebViewHandler] MapSource: {source?.GetType().Name ?? "null"}");
if (source is UrlWebViewSource urlSource)
{
var url = urlSource.Url;
if (!string.IsNullOrEmpty(url))
{
handler._platformWebView.Navigate(url);
}
}
else if (source is HtmlWebViewSource htmlSource)
{
var html = htmlSource.Html;
if (!string.IsNullOrEmpty(html))
{
handler._platformWebView.LoadHtml(html, htmlSource.BaseUrl);
}
}
}
public static void MapGoBack(GtkWebViewHandler handler, IWebView webView, object? args)
{
Console.WriteLine($"[GtkWebViewHandler] MapGoBack called, CanGoBack={handler._platformWebView?.CanGoBack()}");
handler._platformWebView?.GoBack();
}
public static void MapGoForward(GtkWebViewHandler handler, IWebView webView, object? args)
{
Console.WriteLine($"[GtkWebViewHandler] MapGoForward called, CanGoForward={handler._platformWebView?.CanGoForward()}");
handler._platformWebView?.GoForward();
}
public static void MapReload(GtkWebViewHandler handler, IWebView webView, object? args)
{
Console.WriteLine("[GtkWebViewHandler] MapReload called");
handler._platformWebView?.Reload();
}
}

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

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,6 +1,8 @@
// 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 SkiaSharp;
@@ -20,6 +22,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 +94,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 +116,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;
Console.WriteLine($"[ImageHandler] MapWidth: {img.WidthRequest}");
}
else if (image.Width > 0)
{
handler.PlatformView.WidthRequest = image.Width;
}
}
public static void MapHeight(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img && img.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = img.HeightRequest;
Console.WriteLine($"[ImageHandler] MapHeight: {img.HeightRequest}");
}
else if (image.Height > 0)
{
handler.PlatformView.HeightRequest = image.Height;
}
}
public static void MapHorizontalOptions(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img)
{
handler.PlatformView.HorizontalOptions = img.HorizontalOptions;
}
}
public static void MapVerticalOptions(ImageHandler handler, IImage image)
{
if (handler.PlatformView is null) return;
if (image is Image img)
{
handler.PlatformView.VerticalOptions = img.VerticalOptions;
}
}
@@ -162,6 +231,14 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
await _handler.PlatformView!.LoadFromStreamAsync(stream);
}
}
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 +253,73 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
}
}
}
private static SKBitmap? RenderFontImageSource(FontImageSource fontSource, double requestedWidth, double requestedHeight)
{
string glyph = fontSource.Glyph;
if (string.IsNullOrEmpty(glyph))
{
return null;
}
int size = (int)Math.Max(requestedWidth > 0 ? requestedWidth : 24.0, requestedHeight > 0 ? requestedHeight : 24.0);
size = Math.Max(size, 16);
SKColor color = fontSource.Color?.ToSKColor() ?? SKColors.Black;
SKBitmap bitmap = new SKBitmap(size, size, false);
using SKCanvas canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
SKTypeface? typeface = null;
if (!string.IsNullOrEmpty(fontSource.FontFamily))
{
string[] fontPaths = new string[]
{
"/usr/share/fonts/truetype/" + fontSource.FontFamily + ".ttf",
"/usr/share/fonts/opentype/" + fontSource.FontFamily + ".otf",
"/usr/local/share/fonts/" + fontSource.FontFamily + ".ttf",
Path.Combine(AppContext.BaseDirectory, fontSource.FontFamily + ".ttf")
};
foreach (string path in fontPaths)
{
if (File.Exists(path))
{
typeface = SKTypeface.FromFile(path, 0);
if (typeface != null)
{
break;
}
}
}
if (typeface == null)
{
typeface = SKTypeface.FromFamilyName(fontSource.FontFamily);
}
}
if (typeface == null)
{
typeface = SKTypeface.Default;
}
float fontSize = size * 0.8f;
using SKFont font = new SKFont(typeface, fontSize, 1f, 0f);
using SKPaint paint = new SKPaint(font)
{
Color = color,
IsAntialias = true,
TextAlign = SKTextAlign.Center
};
SKRect bounds = default;
paint.MeasureText(glyph, ref bounds);
float x = size / 2f;
float y = (size - bounds.Top - bounds.Bottom) / 2f;
canvas.DrawText(glyph, x, y, paint);
return bitmap;
}
}
}

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,174 +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,
[nameof(ILabel.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
/// <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();
}
public static void MapBackground(LabelHandler handler, ILabel label)
{
if (label.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(LabelHandler handler, ILabel label)
{
if (label is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
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;
@@ -29,6 +32,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
[nameof(IView.Background)] = MapBackground,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
["FormattedText"] = MapFormattedText,
};
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -49,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;
@@ -60,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)
@@ -69,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
};
}
@@ -104,25 +152,23 @@ 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)
@@ -132,16 +178,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
// LineBreakMode is on Label control, not ILabel interface
if (label is Microsoft.Maui.Controls.Label mauiLabel)
{
handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode switch
{
Microsoft.Maui.LineBreakMode.NoWrap => Platform.LineBreakMode.NoWrap,
Microsoft.Maui.LineBreakMode.WordWrap => Platform.LineBreakMode.WordWrap,
Microsoft.Maui.LineBreakMode.CharacterWrap => Platform.LineBreakMode.CharacterWrap,
Microsoft.Maui.LineBreakMode.HeadTruncation => Platform.LineBreakMode.HeadTruncation,
Microsoft.Maui.LineBreakMode.TailTruncation => Platform.LineBreakMode.TailTruncation,
Microsoft.Maui.LineBreakMode.MiddleTruncation => Platform.LineBreakMode.MiddleTruncation,
_ => Platform.LineBreakMode.TailTruncation
};
handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode;
}
}
@@ -161,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)
@@ -174,7 +211,7 @@ 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;
}
}
@@ -205,4 +242,17 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
_ => 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,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
@@ -63,7 +64,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// (e.g., in ItemTemplates for CollectionView)
if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
platformView.BackgroundColor = ve.BackgroundColor;
platformView.Invalidate();
}
@@ -78,7 +79,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
child.Handler = child.ToViewHandler(MauiContext);
}
if (child.Handler?.PlatformView is SkiaView skiaChild)
@@ -98,7 +99,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
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();
}
@@ -107,7 +108,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
{
if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
handler.PlatformView.Invalidate();
}
}
@@ -299,7 +300,7 @@ public partial class GridHandler : LayoutHandler
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
child.Handler = child.ToViewHandler(MauiContext);
Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -53,7 +54,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// 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.ToSKColor();
platformView.BackgroundColor = ve.BackgroundColor;
}
for (int i = 0; i < VirtualView.Count; i++)
@@ -64,7 +65,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
child.Handler = child.ToViewHandler(MauiContext);
}
// Add child's platform view to our layout
@@ -87,7 +88,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;
}
}
@@ -142,12 +143,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
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.Padding = paddable.Padding;
handler.PlatformView.InvalidateMeasure();
handler.PlatformView.Invalidate();
}
@@ -254,18 +250,14 @@ public partial class GridHandler : LayoutHandler
// 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.ToSKColor();
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 = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
platformView.Padding = padding;
Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}");
}
@@ -284,7 +276,7 @@ public partial class GridHandler : LayoutHandler
// Create handler for child if it doesn't exist
if (child.Handler == null)
{
child.Handler = child.ToHandler(MauiContext);
child.Handler = child.ToViewHandler(MauiContext);
}
// Get grid position from attached properties

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,14 @@
// 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 SkiaSharp;
using Svg.Skia;
using System.Collections.Specialized;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -100,7 +103,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (page.Handler == null)
{
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
page.Handler = page.ToHandler(MauiContext);
page.Handler = page.ToViewHandler(MauiContext);
}
Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
@@ -122,7 +125,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToHandler(MauiContext);
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
@@ -163,7 +166,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
contentPage.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems)
{
Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', Order={item.Order}");
Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', IconImageSource={item.IconImageSource}, Order={item.Order}");
// Default and Primary should both be treated as Primary (shown in toolbar)
// Only Secondary goes to overflow menu
var order = item.Order == ToolbarItemOrder.Secondary
@@ -187,9 +190,17 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
}
});
// 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
});
@@ -210,6 +221,56 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
}
}
private SKBitmap? LoadToolbarIcon(string fileName)
{
try
{
string baseDirectory = AppContext.BaseDirectory;
string pngPath = Path.Combine(baseDirectory, fileName);
string svgPath = Path.Combine(baseDirectory, Path.ChangeExtension(fileName, ".svg"));
Console.WriteLine($"[NavigationPageHandler] LoadToolbarIcon: Looking for {fileName}");
Console.WriteLine($"[NavigationPageHandler] Trying PNG: {pngPath} (exists: {File.Exists(pngPath)})");
Console.WriteLine($"[NavigationPageHandler] Trying SVG: {svgPath} (exists: {File.Exists(svgPath)})");
// Try SVG first
if (File.Exists(svgPath))
{
using var svg = new SKSvg();
svg.Load(svgPath);
if (svg.Picture != null)
{
var cullRect = svg.Picture.CullRect;
float scale = 24f / Math.Max(cullRect.Width, cullRect.Height);
var bitmap = new SKBitmap(24, 24, false);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
canvas.Scale(scale);
canvas.DrawPicture(svg.Picture, null);
Console.WriteLine($"[NavigationPageHandler] Loaded SVG icon: {svgPath}");
return bitmap;
}
}
// Try PNG
if (File.Exists(pngPath))
{
using var stream = File.OpenRead(pngPath);
var result = SKBitmap.Decode(stream);
Console.WriteLine($"[NavigationPageHandler] Loaded PNG icon: {pngPath}");
return result;
}
Console.WriteLine($"[NavigationPageHandler] Icon not found: {fileName}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"[NavigationPageHandler] Error loading icon {fileName}: {ex.Message}");
return null;
}
}
private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
try
@@ -221,7 +282,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (e.Page.Handler == null)
{
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
e.Page.Handler = e.Page.ToHandler(MauiContext);
e.Page.Handler = e.Page.ToViewHandler(MauiContext);
Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
}
@@ -231,12 +292,30 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
skiaPage.ShowNavigationBar = true;
skiaPage.TitleBarColor = PlatformView.BarBackgroundColor;
skiaPage.TitleTextColor = PlatformView.BarTextColor;
skiaPage.Title = e.Page.Title ?? "";
// Handle content if null
if (skiaPage.Content == null && e.Page is ContentPage contentPage && contentPage.Content != null)
{
Console.WriteLine($"[NavigationPageHandler] Content is null, creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null)
{
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
}
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{
skiaPage.Content = skiaContent;
Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}");
}
}
Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items");
MapToolbarItems(skiaPage, e.Page);
Console.WriteLine($"[NavigationPageHandler] Pushing page to platform");
PlatformView.Push(skiaPage, true);
Console.WriteLine($"[NavigationPageHandler] Push complete");
PlatformView.Push(skiaPage, false);
Console.WriteLine($"[NavigationPageHandler] Push complete, thread={Environment.CurrentManagedThreadId}");
}
Console.WriteLine("[NavigationPageHandler] OnVirtualViewPushed returning");
}
catch (Exception ex)
{
@@ -250,13 +329,13 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
{
Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}");
// Pop on the platform side to sync with MAUI navigation
PlatformView?.Pop(true);
PlatformView?.Pop();
}
private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e)
{
Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot");
PlatformView?.PopToRoot(true);
PlatformView?.PopToRoot();
}
private void OnPushed(object? sender, NavigationEventArgs e)
@@ -285,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;
}
}
@@ -295,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;
}
}
@@ -305,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;
}
}
@@ -315,7 +394,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (navigationPage.Background is SolidColorBrush solidBrush)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidBrush.Color;
}
}
@@ -334,7 +413,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Ensure handler exists
if (page.Handler == null)
{
page.Handler = page.ToHandler(handler.MauiContext);
page.Handler = page.ToViewHandler(handler.MauiContext);
}
if (page.Handler?.PlatformView is SkiaPage skiaPage)

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.Hosting;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -19,8 +20,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 +49,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,6 +61,7 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
{
platformView.Appearing -= OnAppearing;
platformView.Disappearing -= OnDisappearing;
platformView.MauiPage = null;
base.DisconnectHandler(platformView);
}
@@ -97,9 +106,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;
Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}");
}
}
public static void MapIconImageSource(PageHandler handler, Page page)
{
// Icon is typically used by navigation containers (Shell, TabbedPage)
// Store for later use but don't render directly on the page
handler.PlatformView?.Invalidate();
}
public static void MapIsBusy(PageHandler handler, Page page)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsBusy = page.IsBusy;
}
}
/// <summary>
@@ -111,6 +145,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 =
@@ -132,6 +167,17 @@ 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 || handler.MauiContext is null) return;
@@ -144,7 +190,7 @@ public partial class ContentPageHandler : PageHandler
if (content.Handler == null)
{
Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}");
content.Handler = content.ToHandler(handler.MauiContext);
content.Handler = content.ToViewHandler(handler.MauiContext);
}
// The content's handler should provide the platform view
@@ -163,4 +209,38 @@ public partial class ContentPageHandler : PageHandler
handler.PlatformView.Content = null;
}
}
public static void MapToolbarItems(ContentPageHandler handler, ContentPage page)
{
if (handler.PlatformView is not SkiaContentPage skiaContentPage) return;
SyncToolbarItems(skiaContentPage, page);
}
private static void SyncToolbarItems(SkiaContentPage platformView, ContentPage page)
{
platformView.ToolbarItems.Clear();
foreach (var item in page.ToolbarItems)
{
var skiaItem = new SkiaToolbarItem
{
Text = item.Text ?? "",
Command = item.Command,
Order = item.Order == ToolbarItemOrder.Primary
? SkiaToolbarItemOrder.Primary
: SkiaToolbarItemOrder.Secondary
};
// Load icon if present
if (item.IconImageSource is FileImageSource fileSource)
{
// Icon loading would be async - simplified for now
Console.WriteLine($"[ContentPageHandler] Toolbar item icon: {fileSource.File}");
}
platformView.ToolbarItems.Add(skiaItem);
}
platformView.Invalidate();
}
}

View File

@@ -5,13 +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>
{
@@ -22,10 +22,12 @@ 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,
};
@@ -62,8 +64,17 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
_itemsCollection.CollectionChanged += OnItemsCollectionChanged;
}
// Load items
// 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)
@@ -84,11 +95,14 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
ReloadItems();
}
private void OnSelectedIndexChanged(object? sender, EventArgs e)
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()
@@ -110,38 +124,68 @@ 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;
if (handler.PlatformView.SelectedIndex != picker.SelectedIndex)
{
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
}
}
public static void MapTextColor(PickerHandler handler, IPicker picker)
{
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)
@@ -150,10 +194,17 @@ 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,63 +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,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
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();
}
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
{
if (progress.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress)
{
if (progress is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
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

@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -52,7 +53,7 @@ public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView
// Create handler for content if it doesn't exist
if (content.Handler == null)
{
content.Handler = content.ToHandler(handler.MauiContext);
content.Handler = content.ToViewHandler(handler.MauiContext);
}
if (content.Handler?.PlatformView is SkiaView skiaContent)

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

@@ -4,6 +4,7 @@
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 +14,27 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
/// </summary>
public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{
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<Shell, ShellHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
["GoToAsync"] = MapGoToAsync,
};
public ShellHandler() : base(Mapper, CommandMapper)
@@ -32,20 +48,32 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
protected override SkiaShell CreatePlatformView()
{
Console.WriteLine("[ShellHandler] CreatePlatformView - creating SkiaShell");
return new SkiaShell();
}
protected override void ConnectHandler(SkiaShell platformView)
{
Console.WriteLine("[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();
}
}
@@ -53,6 +81,9 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{
platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged;
platformView.Navigated -= OnNavigated;
platformView.MauiShell = null;
platformView.ContentRenderer = null;
platformView.ColorRefresher = null;
if (VirtualView != null)
{
@@ -65,10 +96,20 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
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 platform navigation events
}
@@ -90,4 +131,289 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
{
Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}");
}
private void SyncShellItems()
{
if (PlatformView is null || VirtualView is null || MauiContext is null) return;
// Clear existing sections
foreach (var section in PlatformView.Sections.ToList())
{
PlatformView.RemoveSection(section);
}
// Add shell items as sections
foreach (var item in VirtualView.Items)
{
if (item is FlyoutItem flyoutItem)
{
var section = new Platform.ShellSection
{
Route = flyoutItem.Route ?? flyoutItem.Title ?? "",
Title = flyoutItem.Title ?? "",
IconPath = flyoutItem.Icon?.ToString()
};
// Add shell contents as items
foreach (var shellSection in flyoutItem.Items)
{
foreach (var content in shellSection.Items)
{
var contentItem = new Platform.ShellContent
{
Route = content.Route ?? content.Title ?? "",
Title = content.Title ?? "",
IconPath = content.Icon?.ToString(),
MauiShellContent = content,
Content = RenderShellContent(content)
};
section.Items.Add(contentItem);
}
}
PlatformView.AddSection(section);
}
else if (item is ShellItem shellItem)
{
var section = new Platform.ShellSection
{
Route = shellItem.Route ?? shellItem.Title ?? "",
Title = shellItem.Title ?? "",
IconPath = shellItem.Icon?.ToString()
};
foreach (var shellSection in shellItem.Items)
{
foreach (var content in shellSection.Items)
{
var contentItem = new Platform.ShellContent
{
Route = content.Route ?? content.Title ?? "",
Title = content.Title ?? "",
IconPath = content.Icon?.ToString(),
MauiShellContent = content,
Content = RenderShellContent(content)
};
section.Items.Add(contentItem);
}
}
PlatformView.AddSection(section);
}
}
}
private SkiaView? RenderShellContent(Microsoft.Maui.Controls.ShellContent content)
{
if (MauiContext is null) return null;
try
{
var page = content.Content as Page;
if (page == null && content.ContentTemplate != null)
{
page = content.ContentTemplate.CreateContent() as Page;
}
if (page != null)
{
if (page.Handler == null)
{
page.Handler = page.ToViewHandler(MauiContext);
}
if (page.Handler?.PlatformView is SkiaView skiaView)
{
return skiaView;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[ShellHandler] Error rendering content: {ex.Message}");
}
return null;
}
private static void RefreshShellColors(SkiaShell platformView, Shell shell)
{
// Sync flyout colors
if (shell.FlyoutBackgroundColor is Color flyoutBgColor)
{
platformView.FlyoutBackgroundColor = flyoutBgColor;
}
else if (shell.FlyoutBackground is SolidColorBrush flyoutBrush)
{
platformView.FlyoutBackgroundColor = flyoutBrush.Color;
}
// Sync nav bar colors
if (shell.BackgroundColor is Color bgColor)
{
platformView.NavBarBackgroundColor = bgColor;
}
}
public static void MapFlyoutIsPresented(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler._isUpdatingFlyoutPresented) return;
try
{
handler._isUpdatingFlyoutPresented = true;
handler.PlatformView.FlyoutIsPresented = shell.FlyoutIsPresented;
}
finally
{
handler._isUpdatingFlyoutPresented = false;
}
}
public static void MapFlyoutBehavior(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.FlyoutBehavior = shell.FlyoutBehavior switch
{
Microsoft.Maui.FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
Microsoft.Maui.FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout,
Microsoft.Maui.FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
_ => ShellFlyoutBehavior.Flyout
};
}
public static void MapFlyoutWidth(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.FlyoutWidth = (float)shell.FlyoutWidth;
}
public static void MapFlyoutBackgroundColor(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.FlyoutBackgroundColor is Color color)
{
handler.PlatformView.FlyoutBackgroundColor = color;
}
}
public static void MapFlyoutBackground(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.FlyoutBackground is SolidColorBrush solidBrush)
{
handler.PlatformView.FlyoutBackgroundColor = solidBrush.Color;
}
}
public static void MapBackgroundColor(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
if (shell.BackgroundColor is Color color)
{
handler.PlatformView.NavBarBackgroundColor = color;
}
}
public static void MapFlyoutHeaderBehavior(ShellHandler handler, Shell shell)
{
// Flyout header behavior - handled by platform view
}
public static void MapFlyoutHeader(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var header = shell.FlyoutHeader;
if (header == null)
{
handler.PlatformView.FlyoutHeaderView = null;
return;
}
if (header is View headerView)
{
if (headerView.Handler == null)
{
headerView.Handler = headerView.ToViewHandler(handler.MauiContext);
}
if (headerView.Handler?.PlatformView is SkiaView skiaHeader)
{
handler.PlatformView.FlyoutHeaderView = skiaHeader;
}
}
}
public static void MapFlyoutFooter(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null || handler.MauiContext is null) return;
var footer = shell.FlyoutFooter;
if (footer == null)
{
handler.PlatformView.FlyoutFooterText = null;
return;
}
// Simple text footer support
if (footer is Label label)
{
handler.PlatformView.FlyoutFooterText = label.Text;
}
else if (footer is string text)
{
handler.PlatformView.FlyoutFooterText = text;
}
}
public static void MapItems(ShellHandler handler, Shell shell)
{
handler.SyncShellItems();
}
public static void MapCurrentItem(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
// Sync current item selection
var currentItem = shell.CurrentItem;
if (currentItem != null)
{
// Find matching section index
for (int i = 0; i < handler.PlatformView.Sections.Count; i++)
{
var section = handler.PlatformView.Sections[i];
if (section.Route == (currentItem.Route ?? currentItem.Title))
{
handler.PlatformView.NavigateToSection(i);
break;
}
}
}
}
public static void MapTitle(ShellHandler handler, Shell shell)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Title = shell.Title ?? "";
}
public static void MapGoToAsync(ShellHandler handler, Shell shell, object? args)
{
if (handler.PlatformView is null || args is null) return;
if (args is ShellNavigationState state)
{
handler.PlatformView.GoToAsync(state.Location.ToString());
}
else if (args is string route)
{
handler.PlatformView.GoToAsync(route);
}
}
}

View File

@@ -1,123 +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,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
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();
}
public static void MapBackground(SliderHandler handler, ISlider slider)
{
if (slider.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(SliderHandler handler, ISlider slider)
{
if (slider is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -68,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;
@@ -112,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)
@@ -131,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)
@@ -140,7 +138,7 @@ 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;
}
}

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,15 +76,21 @@ 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;
if (Math.Abs(handler.PlatformView.Value - stepper.Value) > 0.0001)
handler.PlatformView.Value = stepper.Value;
}
@@ -83,7 +112,24 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
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,94 +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,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
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();
}
public static void MapBackground(SwitchHandler handler, ISwitch @switch)
{
if (@switch.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
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,207 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Linux handler for WebView control using WebKitGTK.
/// </summary>
public partial class WebViewHandler : ViewHandler<IWebView, LinuxWebView>
{
/// <summary>
/// Property mapper for WebView properties.
/// </summary>
public static IPropertyMapper<IWebView, WebViewHandler> Mapper = new PropertyMapper<IWebView, WebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IWebView.Source)] = MapSource,
[nameof(IWebView.UserAgent)] = MapUserAgent,
};
/// <summary>
/// Command mapper for WebView commands.
/// </summary>
public static CommandMapper<IWebView, WebViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
[nameof(IWebView.GoBack)] = MapGoBack,
[nameof(IWebView.GoForward)] = MapGoForward,
[nameof(IWebView.Reload)] = MapReload,
[nameof(IWebView.Eval)] = MapEval,
[nameof(IWebView.EvaluateJavaScriptAsync)] = MapEvaluateJavaScriptAsync,
};
public WebViewHandler() : base(Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public WebViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override LinuxWebView CreatePlatformView()
{
Console.WriteLine("[WebViewHandler] Creating LinuxWebView");
return new LinuxWebView();
}
protected override void ConnectHandler(LinuxWebView platformView)
{
base.ConnectHandler(platformView);
platformView.Navigating += OnNavigating;
platformView.Navigated += OnNavigated;
// Map initial properties
if (VirtualView != null)
{
MapSource(this, VirtualView);
MapUserAgent(this, VirtualView);
}
Console.WriteLine("[WebViewHandler] Handler connected");
}
protected override void DisconnectHandler(LinuxWebView platformView)
{
platformView.Navigating -= OnNavigating;
platformView.Navigated -= OnNavigated;
base.DisconnectHandler(platformView);
Console.WriteLine("[WebViewHandler] Handler disconnected");
}
private void OnNavigating(object? sender, WebViewNavigatingEventArgs e)
{
if (VirtualView == null)
return;
// Notify the virtual view about navigation starting
VirtualView.Navigating(WebNavigationEvent.NewPage, e.Url);
}
private void OnNavigated(object? sender, WebViewNavigatedEventArgs e)
{
if (VirtualView == null)
return;
// Notify the virtual view about navigation completed
var result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure;
VirtualView.Navigated(WebNavigationEvent.NewPage, e.Url, result);
}
#region Property Mappers
public static void MapSource(WebViewHandler handler, IWebView webView)
{
var source = webView.Source;
if (source == null)
return;
Console.WriteLine($"[WebViewHandler] MapSource: {source.GetType().Name}");
if (source is IUrlWebViewSource urlSource && !string.IsNullOrEmpty(urlSource.Url))
{
handler.PlatformView?.LoadUrl(urlSource.Url);
}
else if (source is IHtmlWebViewSource htmlSource && !string.IsNullOrEmpty(htmlSource.Html))
{
handler.PlatformView?.LoadHtml(htmlSource.Html, htmlSource.BaseUrl);
}
}
public static void MapUserAgent(WebViewHandler handler, IWebView webView)
{
if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent))
{
handler.PlatformView.UserAgent = webView.UserAgent;
Console.WriteLine($"[WebViewHandler] MapUserAgent: {webView.UserAgent}");
}
}
#endregion
#region Command Mappers
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
{
if (handler.PlatformView?.CanGoBack == true)
{
handler.PlatformView.GoBack();
Console.WriteLine("[WebViewHandler] GoBack");
}
}
public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args)
{
if (handler.PlatformView?.CanGoForward == true)
{
handler.PlatformView.GoForward();
Console.WriteLine("[WebViewHandler] GoForward");
}
}
public static void MapReload(WebViewHandler handler, IWebView webView, object? args)
{
handler.PlatformView?.Reload();
Console.WriteLine("[WebViewHandler] Reload");
}
public static void MapEval(WebViewHandler handler, IWebView webView, object? args)
{
if (args is string script)
{
handler.PlatformView?.Eval(script);
Console.WriteLine($"[WebViewHandler] Eval: {script.Substring(0, Math.Min(50, script.Length))}...");
}
}
public static void MapEvaluateJavaScriptAsync(WebViewHandler handler, IWebView webView, object? args)
{
if (args is EvaluateJavaScriptAsyncRequest request)
{
var result = handler.PlatformView?.EvaluateJavaScriptAsync(request.Script);
if (result != null)
{
result.ContinueWith(t =>
{
request.SetResult(t.Result);
});
}
else
{
request.SetResult(null);
}
Console.WriteLine($"[WebViewHandler] EvaluateJavaScriptAsync: {request.Script.Substring(0, Math.Min(50, request.Script.Length))}...");
}
}
#endregion
}
/// <summary>
/// Request object for async JavaScript evaluation.
/// </summary>
public class EvaluateJavaScriptAsyncRequest
{
public string Script { get; }
private readonly TaskCompletionSource<string?> _tcs = new();
public EvaluateJavaScriptAsyncRequest(string script)
{
Script = script;
}
public Task<string?> Task => _tcs.Task;
public void SetResult(string? result)
{
_tcs.TrySetResult(result);
}
}

View File

@@ -15,6 +15,7 @@ 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)
@@ -22,6 +23,8 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
[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)
@@ -54,29 +57,63 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
base.DisconnectHandler(platformView);
}
private void OnNavigating(object? sender, WebNavigatingEventArgs e)
private void OnNavigating(object? sender, Microsoft.Maui.Platform.WebNavigatingEventArgs e)
{
// Forward to virtual view if needed
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, WebNavigatedEventArgs e)
private void OnNavigated(object? sender, Microsoft.Maui.Platform.WebNavigatedEventArgs e)
{
// Forward to virtual view if needed
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)
{
if (handler.PlatformView == null) return;
Console.WriteLine("[WebViewHandler] MapSource called");
if (handler.PlatformView == null)
{
Console.WriteLine("[WebViewHandler] PlatformView is null!");
return;
}
var source = webView.Source;
Console.WriteLine($"[WebViewHandler] Source type: {source?.GetType().Name ?? "null"}");
if (source is UrlWebViewSource urlSource)
{
Console.WriteLine($"[WebViewHandler] Loading URL: {urlSource.Url}");
handler.PlatformView.Source = urlSource.Url ?? "";
}
else if (source is HtmlWebViewSource htmlSource)
{
Console.WriteLine($"[WebViewHandler] Loading HTML ({htmlSource.Html?.Length ?? 0} chars)");
Console.WriteLine($"[WebViewHandler] HTML preview: {htmlSource.Html?.Substring(0, Math.Min(100, htmlSource.Html?.Length ?? 0))}...");
handler.PlatformView.Html = htmlSource.Html ?? "";
}
else
{
Console.WriteLine("[WebViewHandler] Unknown source type or null");
}
}
public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args)
@@ -93,4 +130,66 @@ public partial class WebViewHandler : ViewHandler<IWebView, SkiaWebView>
{
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

@@ -81,13 +81,20 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
public static void MapContent(WindowHandler handler, IWindow window)
{
Console.Error.WriteLine($"[WindowHandler] MapContent - PlatformView={handler.PlatformView != null}");
if (handler.PlatformView is null) return;
var content = window.Content;
Console.Error.WriteLine($"[WindowHandler] MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
if (content?.Handler?.PlatformView is SkiaView skiaContent)
{
Console.Error.WriteLine($"[WindowHandler] MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
handler.PlatformView.Content = skiaContent;
}
else
{
Console.Error.WriteLine($"[WindowHandler] MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
}
}
public static void MapX(WindowHandler handler, IWindow window)
@@ -177,8 +184,8 @@ public class SkiaWindow
// Draw main content
if (_content != null)
{
_content.Measure(new SKSize(_width, _height));
_content.Arrange(new SKRect(0, 0, _width, _height));
_content.Measure(new Size(_width, _height));
_content.Arrange(new Rect(0, 0, _width, _height));
_content.Draw(canvas);
}

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

@@ -7,37 +7,41 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.ApplicationModel.Communication;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Converters;
using Microsoft.Maui.Storage;
using Microsoft.Maui.Platform.Linux.Handlers;
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 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>();
@@ -50,6 +54,29 @@ 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();
@@ -77,11 +104,12 @@ public static class LinuxMauiAppBuilderExtensions
handlers.AddHandler<VerticalStackLayout, StackLayoutHandler>();
handlers.AddHandler<HorizontalStackLayout, StackLayoutHandler>();
handlers.AddHandler<AbsoluteLayout, LayoutHandler>();
handlers.AddHandler<FlexLayout, 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>();
// Picker controls
handlers.AddHandler<Picker, PickerHandler>();
@@ -98,9 +126,15 @@ public static class LinuxMauiAppBuilderExtensions
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>();
// Pages & Navigation
handlers.AddHandler<Page, PageHandler>();
@@ -121,33 +155,11 @@ public static class LinuxMauiAppBuilderExtensions
return builder;
}
/// <summary>
/// Registers custom type converters for Linux platform.
/// </summary>
private static void RegisterTypeConverters()
{
// Register SkiaSharp type converters for XAML styling support
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)));
}
}
/// <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
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
}
}

View File

@@ -4,15 +4,10 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform;
using SkiaSharp;
using Microsoft.Maui.Platform.Linux.Dispatching;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Linux-specific implementation of IMauiContext.
/// Provides the infrastructure for creating handlers and accessing platform services.
/// </summary>
public class LinuxMauiContext : IMauiContext
{
private readonly IServiceProvider _services;
@@ -21,27 +16,12 @@ public class LinuxMauiContext : IMauiContext
private IAnimationManager? _animationManager;
private IDispatcher? _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>();
}
/// <inheritdoc />
public IServiceProvider Services => _services;
/// <inheritdoc />
public IMauiHandlersFactory Handlers => _handlers;
/// <summary>
/// Gets the Linux application instance.
/// </summary>
public LinuxApplication LinuxApp => _linuxApp;
/// <summary>
/// Gets the animation manager.
/// </summary>
public IAnimationManager AnimationManager
{
get
@@ -52,9 +32,6 @@ public class LinuxMauiContext : IMauiContext
}
}
/// <summary>
/// Gets the dispatcher for UI thread operations.
/// </summary>
public IDispatcher Dispatcher
{
get
@@ -64,236 +41,11 @@ public class LinuxMauiContext : IMauiContext
return _dispatcher;
}
}
}
/// <summary>
/// Scoped MAUI context for a specific window or view hierarchy.
/// </summary>
public class ScopedLinuxMauiContext : IMauiContext
{
private readonly LinuxMauiContext _parent;
public ScopedLinuxMauiContext(LinuxMauiContext parent)
public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp)
{
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
}
public IServiceProvider Services => _parent.Services;
public IMauiHandlersFactory Handlers => _parent.Handlers;
}
/// <summary>
/// Linux dispatcher for UI thread operations.
/// </summary>
internal class LinuxDispatcher : IDispatcher
{
private readonly object _lock = new();
private readonly Queue<Action> _queue = new();
private bool _isDispatching;
public bool IsDispatchRequired => false; // Linux uses single-threaded event loop
public IDispatcherTimer CreateTimer()
{
return new LinuxDispatcherTimer();
}
public bool Dispatch(Action action)
{
if (action == null)
return false;
lock (_lock)
{
_queue.Enqueue(action);
}
ProcessQueue();
return true;
}
public bool DispatchDelayed(TimeSpan delay, Action action)
{
if (action == null)
return false;
Task.Delay(delay).ContinueWith(_ => Dispatch(action));
return true;
}
private void ProcessQueue()
{
if (_isDispatching)
return;
_isDispatching = true;
try
{
while (true)
{
Action? action;
lock (_lock)
{
if (_queue.Count == 0)
break;
action = _queue.Dequeue();
}
action?.Invoke();
}
}
finally
{
_isDispatching = false;
}
}
}
/// <summary>
/// Linux dispatcher timer implementation.
/// </summary>
internal class LinuxDispatcherTimer : IDispatcherTimer
{
private Timer? _timer;
private TimeSpan _interval = TimeSpan.FromMilliseconds(16); // ~60fps default
private bool _isRunning;
private bool _isRepeating = true;
public TimeSpan Interval
{
get => _interval;
set => _interval = value;
}
public bool IsRunning => _isRunning;
public bool IsRepeating
{
get => _isRepeating;
set => _isRepeating = value;
}
public event EventHandler? Tick;
public void Start()
{
if (_isRunning)
return;
_isRunning = true;
_timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan);
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Tick?.Invoke(this, EventArgs.Empty);
if (!_isRepeating)
{
Stop();
}
}
}
/// <summary>
/// Linux animation manager.
/// </summary>
internal class LinuxAnimationManager : IAnimationManager
{
private readonly List<Microsoft.Maui.Animations.Animation> _animations = new();
private readonly ITicker _ticker;
public LinuxAnimationManager(ITicker ticker)
{
_ticker = ticker;
_ticker.Fire = OnTickerFire;
}
public double SpeedModifier { get; set; } = 1.0;
public bool AutoStartTicker { get; set; } = true;
public ITicker Ticker => _ticker;
public void Add(Microsoft.Maui.Animations.Animation animation)
{
_animations.Add(animation);
if (AutoStartTicker && !_ticker.IsRunning)
{
_ticker.Start();
}
}
public void Remove(Microsoft.Maui.Animations.Animation animation)
{
_animations.Remove(animation);
if (_animations.Count == 0 && _ticker.IsRunning)
{
_ticker.Stop();
}
}
private void OnTickerFire()
{
var animations = _animations.ToArray();
foreach (var animation in animations)
{
animation.Tick(16.0 / 1000.0 * SpeedModifier); // ~60fps
if (animation.HasFinished)
{
Remove(animation);
}
}
}
}
/// <summary>
/// Linux ticker for animation timing.
/// </summary>
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)
return;
_isRunning = true;
var interval = TimeSpan.FromMilliseconds(1000.0 / _maxFps);
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, interval);
}
public void Stop()
{
_isRunning = false;
_timer?.Dispose();
_timer = null;
}
private void OnTimerCallback(object? state)
{
Fire?.Invoke();
_services = services ?? throw new ArgumentNullException(nameof(services));
_linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp));
_handlers = services.GetRequiredService<IMauiHandlersFactory>();
}
}

View File

@@ -2,9 +2,11 @@
// 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.Platform;
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;
@@ -44,6 +46,10 @@ public static class LinuxProgramHost
?? new LinuxApplicationOptions();
ParseCommandLineOptions(args, options);
// Initialize GTK for WebView support
GtkHostService.Instance.Initialize(options.Title ?? "MAUI Application", options.Width, options.Height);
Console.WriteLine("[LinuxProgramHost] GTK initialized for WebView support");
// Create Linux application
using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options);
@@ -186,33 +192,33 @@ public static class LinuxProgramHost
{
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 = "OpenMaui Linux Control Demo",
FontSize = 28,
TextColor = new SKColor(0x1A, 0x23, 0x7E),
IsBold = true
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
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 ==========
@@ -221,20 +227,20 @@ public static class LinuxProgramHost
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);
@@ -249,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);
@@ -262,7 +268,7 @@ public static class LinuxProgramHost
{
Placeholder = "Enter multiple lines of text...",
FontSize = 14,
BackgroundColor = SKColors.White
BackgroundColor = Colors.White
};
root.AddChild(editor);
@@ -324,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());
@@ -332,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 ==========
@@ -340,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);
@@ -349,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);
@@ -358,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);
@@ -370,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);
@@ -395,7 +401,7 @@ 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 };
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = Colors.Gray };
collectionView.SelectionChanged += (s, e) =>
{
var selected = e.CurrentSelection.FirstOrDefault();
@@ -413,18 +419,15 @@ public static class LinuxProgramHost
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);
@@ -440,7 +443,7 @@ public static class LinuxProgramHost
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 ==========
@@ -449,14 +452,14 @@ public static class LinuxProgramHost
{
Text = "All 25+ controls are interactive - try them all!",
FontSize = 16,
TextColor = new SKColor(0x4C, 0xAF, 0x50),
IsBold = true
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
TextColor = Colors.Gray
});
scroll.Content = root;
@@ -469,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

@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using SkiaSharp;
@@ -88,29 +90,19 @@ public class LinuxViewRenderer
try
{
// Render the page content
SkiaView? pageContent = null;
if (page is ContentPage contentPage && contentPage.Content != null)
{
pageContent = CurrentRenderer.RenderView(contentPage.Content);
}
// Render the page through the proper handler system
// This ensures all properties (including BackgroundColor via AppThemeBinding) are mapped
var skiaPage = CurrentRenderer.RenderPage(page);
if (pageContent == null)
if (skiaPage == null)
{
Console.WriteLine($"[PushPage] Failed to render page content");
Console.WriteLine($"[PushPage] Failed to render page through handler");
return false;
}
// Wrap in ScrollView if needed
if (pageContent is not SkiaScrollView)
{
var scrollView = new SkiaScrollView { Content = pageContent };
pageContent = scrollView;
}
// Push onto SkiaShell's navigation stack
CurrentSkiaShell.PushAsync(pageContent, page.Title ?? "Detail");
Console.WriteLine($"[PushPage] Successfully pushed page");
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail");
Console.WriteLine($"[PushPage] Successfully pushed page via handler system");
return true;
}
catch (Exception ex)
@@ -162,18 +154,11 @@ public class LinuxViewRenderer
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)
{
// For ContentPage, render the content
if (page is ContentPage contentPage && contentPage.Content != null)
{
var contentView = RenderView(contentPage.Content);
if (skiaPage is SkiaPage sp && contentView != null)
{
sp.Content = contentView;
}
}
return skiaPage;
}
@@ -198,9 +183,41 @@ public class LinuxViewRenderer
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)
{
@@ -210,6 +227,10 @@ public class LinuxViewRenderer
// Store reference to SkiaShell for navigation
CurrentSkiaShell = skiaShell;
// Set up content renderer and color refresher delegates
skiaShell.ContentRenderer = CreateShellContentPage;
skiaShell.ColorRefresher = ApplyShellColors;
// Subscribe to MAUI Shell navigation events to update SkiaShell
shell.Navigated += OnShellNavigated;
shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}");
@@ -223,6 +244,51 @@ public class LinuxViewRenderer
return skiaShell;
}
/// <summary>
/// Applies shell colors based on the current theme (dark/light mode).
/// </summary>
private static void ApplyShellColors(SkiaShell skiaShell, Shell shell)
{
bool isDark = Application.Current?.UserAppTheme == AppTheme.Dark;
Console.WriteLine($"[ApplyShellColors] Theme is: {(isDark ? "Dark" : "Light")}");
// Flyout background color
if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent)
{
skiaShell.FlyoutBackgroundColor = shell.FlyoutBackgroundColor;
Console.WriteLine($"[ApplyShellColors] FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
}
else
{
skiaShell.FlyoutBackgroundColor = isDark
? Color.FromRgb(30, 30, 30)
: Color.FromRgb(255, 255, 255);
Console.WriteLine($"[ApplyShellColors] Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
}
// Flyout text color
skiaShell.FlyoutTextColor = isDark
? Color.FromRgb(224, 224, 224)
: Color.FromRgb(33, 33, 33);
Console.WriteLine($"[ApplyShellColors] FlyoutTextColor: {skiaShell.FlyoutTextColor}");
// Content background color
skiaShell.ContentBackgroundColor = isDark
? Color.FromRgb(18, 18, 18)
: Color.FromRgb(250, 250, 250);
Console.WriteLine($"[ApplyShellColors] ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
// NavBar background color
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
{
skiaShell.NavBarBackgroundColor = shell.BackgroundColor;
}
else
{
skiaShell.NavBarBackgroundColor = Color.FromRgb(33, 150, 243); // Material blue
}
}
/// <summary>
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
/// </summary>
@@ -290,7 +356,8 @@ public class LinuxViewRenderer
var shellContent = new ShellContent
{
Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "",
Route = content.Route ?? ""
Route = content.Route ?? "",
MauiShellContent = content
};
// Create the page content
@@ -328,7 +395,8 @@ public class LinuxViewRenderer
var shellContent = new ShellContent
{
Title = content.Title ?? tab.Title ?? "",
Route = content.Route ?? ""
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
@@ -359,7 +427,8 @@ public class LinuxViewRenderer
var shellContent = new ShellContent
{
Title = content.Title ?? "",
Route = content.Route ?? ""
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
@@ -402,17 +471,33 @@ public class LinuxViewRenderer
var contentView = RenderView(cp.Content);
if (contentView != null)
{
if (contentView is SkiaScrollView)
// Get page background color if set
Color? bgColor = null;
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
{
return contentView;
bgColor = cp.BackgroundColor;
Console.WriteLine($"[CreateShellContentPage] Page BackgroundColor: {bgColor}");
}
if (contentView is SkiaScrollView scrollView)
{
if (bgColor != null)
{
scrollView.BackgroundColor = bgColor;
}
return scrollView;
}
else
{
var scrollView = new SkiaScrollView
var newScrollView = new SkiaScrollView
{
Content = contentView
};
return scrollView;
if (bgColor != null)
{
newScrollView.BackgroundColor = bgColor;
}
return newScrollView;
}
}
}
@@ -470,28 +555,9 @@ public class LinuxViewRenderer
return new SkiaLabel
{
Text = $"[{view.GetType().Name}]",
TextColor = SKColors.Gray,
TextColor = Colors.Gray,
FontSize = 12
};
}
}
/// <summary>
/// Extension methods for MAUI handler creation.
/// </summary>
public static class MauiHandlerExtensions
{
/// <summary>
/// Creates a handler for the view and returns it.
/// </summary>
public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext)
{
var handler = mauiContext.Handlers.GetHandler(element.GetType());
if (handler != null)
{
handler.SetMauiContext(mauiContext);
handler.SetVirtualView(element);
}
return handler!;
}
}

View File

@@ -1,190 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Copyright (c) 2025 MarketAlly LLC
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers;
namespace OpenMaui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for configuring OpenMaui Linux platform in a MAUI application.
/// This enables full XAML support by registering Linux-specific handlers.
/// </summary>
public static class MauiAppBuilderExtensions
{
/// <summary>
/// Configures the application to use OpenMaui Linux platform with full XAML support.
/// </summary>
/// <param name="builder">The MAUI app builder.</param>
/// <returns>The configured MAUI app builder.</returns>
/// <example>
/// <code>
/// var builder = MauiApp.CreateBuilder();
/// builder
/// .UseMauiApp&lt;App&gt;()
/// .UseOpenMauiLinux(); // Enable Linux support with XAML
/// </code>
/// </example>
public static MauiAppBuilder UseOpenMauiLinux(this MauiAppBuilder builder)
{
builder.ConfigureMauiHandlers(handlers =>
{
// Register all Linux platform handlers
// These map MAUI virtual views to our Skia platform views
// Basic Controls
handlers.AddHandler<Button, ButtonHandler>();
handlers.AddHandler<Label, LabelHandler>();
handlers.AddHandler<Entry, EntryHandler>();
handlers.AddHandler<Editor, EditorHandler>();
handlers.AddHandler<CheckBox, CheckBoxHandler>();
handlers.AddHandler<Switch, SwitchHandler>();
handlers.AddHandler<RadioButton, RadioButtonHandler>();
// Selection Controls
handlers.AddHandler<Slider, SliderHandler>();
handlers.AddHandler<Stepper, StepperHandler>();
handlers.AddHandler<Picker, PickerHandler>();
handlers.AddHandler<DatePicker, DatePickerHandler>();
handlers.AddHandler<TimePicker, TimePickerHandler>();
// Display Controls
handlers.AddHandler<Image, ImageHandler>();
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<ActivityIndicator, ActivityIndicatorHandler>();
handlers.AddHandler<ProgressBar, ProgressBarHandler>();
// Layout Controls
handlers.AddHandler<Border, BorderHandler>();
// Collection Controls
handlers.AddHandler<CollectionView, CollectionViewHandler>();
// Navigation Controls
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
handlers.AddHandler<TabbedPage, TabbedPageHandler>();
handlers.AddHandler<FlyoutPage, FlyoutPageHandler>();
handlers.AddHandler<Shell, ShellHandler>();
// Page Controls
handlers.AddHandler<Page, PageHandler>();
handlers.AddHandler<ContentPage, PageHandler>();
// Graphics
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
// Search
handlers.AddHandler<SearchBar, SearchBarHandler>();
// Web
handlers.AddHandler<WebView, WebViewHandler>();
// Window
handlers.AddHandler<Window, WindowHandler>();
});
// Register Linux-specific services
builder.Services.AddSingleton<ILinuxPlatformServices, LinuxPlatformServices>();
return builder;
}
/// <summary>
/// Configures the application to use OpenMaui Linux with custom handler configuration.
/// </summary>
/// <param name="builder">The MAUI app builder.</param>
/// <param name="configureHandlers">Action to configure additional handlers.</param>
/// <returns>The configured MAUI app builder.</returns>
public static MauiAppBuilder UseOpenMauiLinux(
this MauiAppBuilder builder,
Action<IMauiHandlersCollection>? configureHandlers)
{
builder.UseOpenMauiLinux();
if (configureHandlers != null)
{
builder.ConfigureMauiHandlers(configureHandlers);
}
return builder;
}
}
/// <summary>
/// Interface for Linux platform services.
/// </summary>
public interface ILinuxPlatformServices
{
/// <summary>
/// Gets the display server type (X11 or Wayland).
/// </summary>
DisplayServerType DisplayServer { get; }
/// <summary>
/// Gets the current DPI scale factor.
/// </summary>
float ScaleFactor { get; }
/// <summary>
/// Gets whether high contrast mode is enabled.
/// </summary>
bool IsHighContrastEnabled { get; }
}
/// <summary>
/// Display server types supported by OpenMaui.
/// </summary>
public enum DisplayServerType
{
/// <summary>X11 display server.</summary>
X11,
/// <summary>Wayland display server.</summary>
Wayland,
/// <summary>Auto-detected display server.</summary>
Auto
}
/// <summary>
/// Implementation of Linux platform services.
/// </summary>
internal class LinuxPlatformServices : ILinuxPlatformServices
{
public DisplayServerType DisplayServer => DetectDisplayServer();
public float ScaleFactor => DetectScaleFactor();
public bool IsHighContrastEnabled => DetectHighContrast();
private static DisplayServerType DetectDisplayServer()
{
var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY");
if (!string.IsNullOrEmpty(waylandDisplay))
return DisplayServerType.Wayland;
var display = Environment.GetEnvironmentVariable("DISPLAY");
if (!string.IsNullOrEmpty(display))
return DisplayServerType.X11;
return DisplayServerType.Auto;
}
private static float DetectScaleFactor()
{
// Try GDK_SCALE first
var gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE");
if (float.TryParse(gdkScale, out var scale))
return scale;
// Default to 1.0
return 1.0f;
}
private static bool DetectHighContrast()
{
var highContrast = Environment.GetEnvironmentVariable("GTK_THEME");
return highContrast?.Contains("HighContrast", StringComparison.OrdinalIgnoreCase) ?? false;
}
}

View File

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

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

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

25
Interop/XCrossingEvent.cs Normal file
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.
namespace Microsoft.Maui.Platform.Linux.Interop;
public struct XCrossingEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public IntPtr Root;
public IntPtr Subwindow;
public ulong Time;
public int X;
public int Y;
public int XRoot;
public int YRoot;
public int Mode;
public int Detail;
public int SameScreen;
public int Focus;
public uint State;
}

13
Interop/XCursorShape.cs Normal file
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.
namespace Microsoft.Maui.Platform.Linux.Interop;
public static class XCursorShape
{
public const uint XC_left_ptr = 68;
public const uint XC_hand2 = 60;
public const uint XC_xterm = 152;
public const uint XC_watch = 150;
public const uint XC_crosshair = 34;
}

37
Interop/XEvent.cs Normal file
View File

@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
[StructLayout(LayoutKind.Explicit, Size = 192)]
public struct XEvent
{
[FieldOffset(0)]
public int Type;
[FieldOffset(0)]
public XKeyEvent KeyEvent;
[FieldOffset(0)]
public XButtonEvent ButtonEvent;
[FieldOffset(0)]
public XMotionEvent MotionEvent;
[FieldOffset(0)]
public XConfigureEvent ConfigureEvent;
[FieldOffset(0)]
public XExposeEvent ExposeEvent;
[FieldOffset(0)]
public XClientMessageEvent ClientMessageEvent;
[FieldOffset(0)]
public XCrossingEvent CrossingEvent;
[FieldOffset(0)]
public XFocusChangeEvent FocusChangeEvent;
}

18
Interop/XEventMask.cs Normal file
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.Interop;
public static class XEventMask
{
public const long KeyPressMask = 1L;
public const long KeyReleaseMask = 2L;
public const long ButtonPressMask = 4L;
public const long ButtonReleaseMask = 8L;
public const long EnterWindowMask = 16L;
public const long LeaveWindowMask = 32L;
public const long PointerMotionMask = 64L;
public const long ExposureMask = 32768L;
public const long StructureNotifyMask = 131072L;
public const long FocusChangeMask = 2097152L;
}

20
Interop/XEventType.cs Normal file
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.
namespace Microsoft.Maui.Platform.Linux.Interop;
public static class XEventType
{
public const int KeyPress = 2;
public const int KeyRelease = 3;
public const int ButtonPress = 4;
public const int ButtonRelease = 5;
public const int MotionNotify = 6;
public const int EnterNotify = 7;
public const int LeaveNotify = 8;
public const int FocusIn = 9;
public const int FocusOut = 10;
public const int Expose = 12;
public const int ConfigureNotify = 22;
public const int ClientMessage = 33;
}

18
Interop/XExposeEvent.cs Normal file
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.Interop;
public struct XExposeEvent
{
public int Type;
public ulong Serial;
public int SendEvent;
public IntPtr Display;
public IntPtr Window;
public int X;
public int Y;
public int Width;
public int Height;
public int Count;
}

View File

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

23
Interop/XKeyEvent.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 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;
public int Y;
public int XRoot;
public int YRoot;
public uint State;
public uint Keycode;
public int SameScreen;
}

23
Interop/XMotionEvent.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 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;
public int Y;
public int XRoot;
public int YRoot;
public uint State;
public byte IsHint;
public int SameScreen;
}

139
Interop/XRandR.cs Normal file
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.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Interop;
/// <summary>
/// XRandR (X Resize and Rotate) extension interop for multi-monitor support.
/// </summary>
internal static partial class XRandR
{
private const string LibXrandr = "libXrandr.so.2";
// RROutput and RRCrtc are XIDs (unsigned long)
// RRMode is also an XID
[LibraryImport(LibXrandr)]
public static partial IntPtr XRRGetScreenResources(IntPtr display, IntPtr window);
[LibraryImport(LibXrandr)]
public static partial IntPtr XRRGetScreenResourcesCurrent(IntPtr display, IntPtr window);
[LibraryImport(LibXrandr)]
public static partial void XRRFreeScreenResources(IntPtr resources);
[LibraryImport(LibXrandr)]
public static partial IntPtr XRRGetOutputInfo(IntPtr display, IntPtr resources, ulong output);
[LibraryImport(LibXrandr)]
public static partial void XRRFreeOutputInfo(IntPtr outputInfo);
[LibraryImport(LibXrandr)]
public static partial IntPtr XRRGetCrtcInfo(IntPtr display, IntPtr resources, ulong crtc);
[LibraryImport(LibXrandr)]
public static partial void XRRFreeCrtcInfo(IntPtr crtcInfo);
[LibraryImport(LibXrandr)]
public static partial int XRRQueryExtension(IntPtr display, out int eventBase, out int errorBase);
[LibraryImport(LibXrandr)]
public static partial int XRRQueryVersion(IntPtr display, out int major, out int minor);
[LibraryImport(LibXrandr)]
public static partial void XRRSelectInput(IntPtr display, IntPtr window, int mask);
// RRNotify mask values
public const int RRScreenChangeNotifyMask = 1 << 0;
public const int RRCrtcChangeNotifyMask = 1 << 1;
public const int RROutputChangeNotifyMask = 1 << 2;
public const int RROutputPropertyNotifyMask = 1 << 3;
// Connection status
public const int RR_Connected = 0;
public const int RR_Disconnected = 1;
public const int RR_UnknownConnection = 2;
}
/// <summary>
/// XRRScreenResources structure layout.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct XRRScreenResources
{
public ulong Timestamp;
public ulong ConfigTimestamp;
public int NCrtc;
public IntPtr Crtcs; // RRCrtc* (array of ulongs)
public int NOutput;
public IntPtr Outputs; // RROutput* (array of ulongs)
public int NMode;
public IntPtr Modes; // XRRModeInfo*
}
/// <summary>
/// XRROutputInfo structure layout.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct XRROutputInfo
{
public ulong Timestamp;
public ulong Crtc; // RRCrtc - current CRTC (0 if not connected)
public IntPtr Name; // char*
public int NameLen;
public ulong MmWidth; // Physical width in mm
public ulong MmHeight; // Physical height in mm
public ushort Connection; // RRConnection status
public ushort SubpixelOrder;
public int NCrtc;
public IntPtr Crtcs; // RRCrtc* - possible CRTCs
public int NClone;
public IntPtr Clones; // RROutput*
public int NMode;
public int NPreferred;
public IntPtr Modes; // RRMode*
}
/// <summary>
/// XRRCrtcInfo structure layout.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct XRRCrtcInfo
{
public ulong Timestamp;
public int X;
public int Y;
public uint Width;
public uint Height;
public ulong Mode; // RRMode - current mode
public ushort Rotation;
public int NOutput;
public IntPtr Outputs; // RROutput*
public ushort Rotations; // Possible rotations
public int NPossible;
public IntPtr Possible; // RROutput*
}
/// <summary>
/// XRRModeInfo structure layout.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct XRRModeInfo
{
public ulong Id; // RRMode
public uint Width;
public uint Height;
public ulong DotClock;
public uint HSyncStart;
public uint HSyncEnd;
public uint HTotal;
public uint HSkew;
public uint VSyncStart;
public uint VSyncEnd;
public uint VTotal;
public IntPtr Name; // char*
public uint NameLength;
public ulong ModeFlags;
}

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

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