24 Commits

Author SHA1 Message Date
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
354 changed files with 19616 additions and 9183 deletions

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,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 +1,63 @@
// 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.Handlers;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for ActivityIndicator control.
/// </summary>
public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator, SkiaActivityIndicator>
public 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,
["IsRunning"] = MapIsRunning,
["Color"] = MapColor,
["Background"] = MapBackground
};
public static CommandMapper<IActivityIndicator, ActivityIndicatorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public ActivityIndicatorHandler() : base(Mapper, CommandMapper) { }
public ActivityIndicatorHandler() : base(Mapper, CommandMapper)
{
}
protected override SkiaActivityIndicator CreatePlatformView() => new SkiaActivityIndicator();
public ActivityIndicatorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaActivityIndicator CreatePlatformView()
{
return new SkiaActivityIndicator();
}
public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
handler.PlatformView.IsRunning = activityIndicator.IsRunning;
if (handler.PlatformView != null)
{
handler.PlatformView.IsRunning = activityIndicator.IsRunning;
}
}
public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
if (activityIndicator.Color != null)
if (handler.PlatformView != null && 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)
if (handler.PlatformView != 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();
if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
}

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;
@@ -48,13 +49,28 @@ 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;
}
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 +84,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)

View File

@@ -1,57 +1,42 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Button control.
/// </summary>
public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
public 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,
["StrokeColor"] = MapStrokeColor,
["StrokeThickness"] = MapStrokeThickness,
["CornerRadius"] = MapCornerRadius,
["Background"] = MapBackground,
["Padding"] = MapPadding,
["IsEnabled"] = MapIsEnabled
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<IButton, ButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
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)
public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaButton CreatePlatformView()
{
var button = new SkiaButton();
return button;
return new SkiaButton();
}
protected override void ConnectHandler(SkiaButton platformView)
@@ -61,18 +46,13 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
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);
MapStrokeColor(this, VirtualView);
MapStrokeThickness(this, VirtualView);
MapCornerRadius(this, VirtualView);
MapBorderColor(this, VirtualView);
MapBorderWidth(this, VirtualView);
MapBackground(this, VirtualView);
MapPadding(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
@@ -100,80 +80,66 @@ public partial class ButtonHandler : ViewHandler<IButton, SkiaButton>
VirtualView?.Released();
}
public static void MapText(ButtonHandler handler, IButton button)
public static void MapStrokeColor(ButtonHandler handler, IButton button)
{
handler.PlatformView.Text = button.Text ?? "";
handler.PlatformView.Invalidate();
}
public static void MapTextColor(ButtonHandler handler, IButton button)
{
if (button.TextColor != null)
if (handler.PlatformView != null)
{
handler.PlatformView.TextColor = button.TextColor.ToSKColor();
var strokeColor = button.StrokeColor;
if (strokeColor != null)
{
handler.PlatformView.BorderColor = strokeColor.ToSKColor();
}
}
handler.PlatformView.Invalidate();
}
public static void MapBackground(ButtonHandler handler, IButton button)
public static void MapStrokeThickness(ButtonHandler handler, IButton button)
{
var background = button.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
if (handler.PlatformView != null)
{
// Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor
handler.PlatformView.ButtonBackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
}
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)
if (handler.PlatformView != null)
{
handler.PlatformView.BorderColor = button.StrokeColor.ToSKColor();
handler.PlatformView.CornerRadius = button.CornerRadius;
}
handler.PlatformView.Invalidate();
}
public static void MapBorderWidth(ButtonHandler handler, IButton button)
public static void MapBackground(ButtonHandler handler, IButton button)
{
handler.PlatformView.BorderWidth = (float)button.StrokeThickness;
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
var background = button.Background;
if (background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapPadding(ButtonHandler handler, IButton button)
{
if (handler.PlatformView != null)
{
var padding = button.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
}
}
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();
if (handler.PlatformView != null)
{
Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}");
handler.PlatformView.IsEnabled = button.IsEnabled;
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -1,45 +1,35 @@
// 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.Graphics;
using Microsoft.Maui.Handlers;
using SkiaSharp;
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for CheckBox control.
/// </summary>
public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
public 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,
["IsChecked"] = MapIsChecked,
["Foreground"] = MapForeground,
["Background"] = MapBackground,
["IsEnabled"] = MapIsEnabled,
["VerticalLayoutAlignment"] = MapVerticalLayoutAlignment,
["HorizontalLayoutAlignment"] = MapHorizontalLayoutAlignment
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<ICheckBox, CheckBoxHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
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)
public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
@@ -71,7 +61,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView.IsChecked != checkBox.IsChecked)
if (handler.PlatformView != null)
{
handler.PlatformView.IsChecked = checkBox.IsChecked;
}
@@ -79,35 +69,61 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox)
{
var foreground = checkBox.Foreground;
if (foreground is SolidColorBrush solidBrush && solidBrush.Color != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BoxColor = solidBrush.Color.ToSKColor();
if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.CheckColor = solidPaint.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)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapBackgroundColor(CheckBoxHandler handler, ICheckBox checkBox)
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
{
if (checkBox is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
}
}
public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView != null)
{
handler.PlatformView.VerticalOptions = (int)checkBox.VerticalLayoutAlignment switch
{
1 => LayoutOptions.Start,
2 => LayoutOptions.Center,
3 => LayoutOptions.End,
0 => LayoutOptions.Fill,
_ => LayoutOptions.Fill
};
}
}
public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{
if (handler.PlatformView != null)
{
handler.PlatformView.HorizontalOptions = (int)checkBox.HorizontalLayoutAlignment switch
{
1 => LayoutOptions.Start,
2 => LayoutOptions.Center,
3 => LayoutOptions.End,
0 => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}
}

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,
};
@@ -86,6 +87,12 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
}
}
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)

View File

@@ -49,6 +49,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 = new SKColor(30, 30, 30);
platformView.TextColor = new SKColor(224, 224, 224);
platformView.BorderColor = new SKColor(97, 97, 97);
platformView.DisabledDayColor = new SKColor(97, 97, 97);
platformView.BackgroundColor = new SKColor(45, 45, 45);
}
}
protected override void DisconnectHandler(SkiaDatePicker platformView)

View File

@@ -0,0 +1,187 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Editor control.
/// </summary>
public class EditorHandler : ViewHandler<IEditor, SkiaEditor>
{
public static IPropertyMapper<IEditor, EditorHandler> Mapper = new PropertyMapper<IEditor, EditorHandler>(ViewHandler.ViewMapper)
{
["Text"] = MapText,
["Placeholder"] = MapPlaceholder,
["PlaceholderColor"] = MapPlaceholderColor,
["TextColor"] = MapTextColor,
["CharacterSpacing"] = MapCharacterSpacing,
["IsReadOnly"] = MapIsReadOnly,
["IsTextPredictionEnabled"] = MapIsTextPredictionEnabled,
["MaxLength"] = MapMaxLength,
["CursorPosition"] = MapCursorPosition,
["SelectionLength"] = MapSelectionLength,
["Keyboard"] = MapKeyboard,
["HorizontalTextAlignment"] = MapHorizontalTextAlignment,
["VerticalTextAlignment"] = MapVerticalTextAlignment,
["Background"] = MapBackground,
["BackgroundColor"] = MapBackgroundColor
};
public static CommandMapper<IEditor, EditorHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public EditorHandler() : base(Mapper, CommandMapper)
{
}
public EditorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaEditor CreatePlatformView()
{
return new SkiaEditor();
}
protected override void ConnectHandler(SkiaEditor platformView)
{
base.ConnectHandler(platformView);
platformView.TextChanged += OnTextChanged;
platformView.Completed += OnCompleted;
}
protected override void DisconnectHandler(SkiaEditor platformView)
{
platformView.TextChanged -= OnTextChanged;
platformView.Completed -= OnCompleted;
base.DisconnectHandler(platformView);
}
private void OnTextChanged(object? sender, EventArgs e)
{
if (VirtualView != null && PlatformView != null)
{
VirtualView.Text = PlatformView.Text;
}
}
private void OnCompleted(object? sender, EventArgs e)
{
// Editor completed - no specific action needed
}
public static void MapText(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Text = editor.Text ?? "";
handler.PlatformView.Invalidate();
}
}
public static void MapPlaceholder(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Placeholder = editor.Placeholder ?? "";
}
}
public static void MapPlaceholderColor(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null && editor.PlaceholderColor != null)
{
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor();
}
}
public static void MapTextColor(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null && editor.TextColor != null)
{
handler.PlatformView.TextColor = editor.TextColor.ToSKColor();
}
}
public static void MapCharacterSpacing(EditorHandler handler, IEditor editor)
{
// Character spacing not implemented for editor
}
public static void MapIsReadOnly(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
handler.PlatformView.IsReadOnly = editor.IsReadOnly;
}
}
public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor)
{
// Text prediction is a mobile feature
}
public static void MapMaxLength(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
handler.PlatformView.MaxLength = editor.MaxLength;
}
}
public static void MapCursorPosition(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
handler.PlatformView.CursorPosition = editor.CursorPosition;
}
}
public static void MapSelectionLength(EditorHandler handler, IEditor editor)
{
// Selection length not implemented
}
public static void MapKeyboard(EditorHandler handler, IEditor editor)
{
// Keyboard type is a mobile feature
}
public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor)
{
// Horizontal text alignment not implemented
}
public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor)
{
// Vertical text alignment not implemented
}
public static void MapBackground(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
if (editor.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapBackgroundColor(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView != null)
{
if (editor is VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}
}

View File

@@ -1,55 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Entry control.
/// </summary>
public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
public 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,
["Text"] = MapText,
["TextColor"] = MapTextColor,
["Font"] = MapFont,
["CharacterSpacing"] = MapCharacterSpacing,
["Placeholder"] = MapPlaceholder,
["PlaceholderColor"] = MapPlaceholderColor,
["IsReadOnly"] = MapIsReadOnly,
["MaxLength"] = MapMaxLength,
["CursorPosition"] = MapCursorPosition,
["SelectionLength"] = MapSelectionLength,
["IsPassword"] = MapIsPassword,
["ReturnType"] = MapReturnType,
["ClearButtonVisibility"] = MapClearButtonVisibility,
["HorizontalTextAlignment"] = MapHorizontalTextAlignment,
["VerticalTextAlignment"] = MapVerticalTextAlignment,
["Background"] = MapBackground,
["BackgroundColor"] = MapBackgroundColor
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
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)
public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
@@ -75,9 +67,9 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (VirtualView != null && VirtualView.Text != e.NewText)
if (VirtualView != null && PlatformView != null && VirtualView.Text != e.NewTextValue)
{
VirtualView.Text = e.NewText;
VirtualView.Text = e.NewTextValue ?? string.Empty;
}
}
@@ -88,112 +80,173 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
public static void MapText(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView.Text != entry.Text)
if (handler.PlatformView != null && handler.PlatformView.Text != entry.Text)
{
handler.PlatformView.Text = entry.Text ?? "";
handler.PlatformView.Text = entry.Text ?? string.Empty;
handler.PlatformView.Invalidate();
}
}
public static void MapTextColor(EntryHandler handler, IEntry entry)
{
if (entry.TextColor != null)
if (handler.PlatformView != null && 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)
if (handler.PlatformView != null)
{
handler.PlatformView.FontFamily = font.Family;
var font = entry.Font;
if (font.Size > 0)
{
handler.PlatformView.FontSize = (float)font.Size;
}
if (!string.IsNullOrEmpty(font.Family))
{
handler.PlatformView.FontFamily = font.Family;
}
handler.PlatformView.IsBold = (int)font.Weight >= 700;
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
}
handler.PlatformView.FontSize = (float)font.Size;
handler.PlatformView.Invalidate();
}
public static void MapIsPassword(EntryHandler handler, IEntry entry)
public static void MapCharacterSpacing(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsPassword = entry.IsPassword;
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
handler.PlatformView.CharacterSpacing = (float)entry.CharacterSpacing;
}
}
public static void MapMaxLength(EntryHandler handler, IEntry entry)
public static void MapPlaceholder(EntryHandler handler, IEntry entry)
{
handler.PlatformView.MaxLength = entry.MaxLength;
if (handler.PlatformView != null)
{
handler.PlatformView.Placeholder = entry.Placeholder ?? string.Empty;
}
}
public static void MapPlaceholderColor(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView != null && entry.PlaceholderColor != null)
{
handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor();
}
}
public static void MapIsReadOnly(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsReadOnly = entry.IsReadOnly;
if (handler.PlatformView != null)
{
handler.PlatformView.IsReadOnly = entry.IsReadOnly;
}
}
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
public static void MapMaxLength(EntryHandler handler, IEntry entry)
{
handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch
if (handler.PlatformView != null)
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
handler.PlatformView.Invalidate();
handler.PlatformView.MaxLength = entry.MaxLength;
}
}
public static void MapCursorPosition(EntryHandler handler, IEntry entry)
{
handler.PlatformView.CursorPosition = entry.CursorPosition;
if (handler.PlatformView != null)
{
handler.PlatformView.CursorPosition = entry.CursorPosition;
}
}
public static void MapSelectionLength(EntryHandler handler, IEntry entry)
{
// Selection length is handled internally by SkiaEntry
if (handler.PlatformView != null)
{
handler.PlatformView.SelectionLength = entry.SelectionLength;
}
}
public static void MapIsPassword(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView != null)
{
handler.PlatformView.IsPassword = entry.IsPassword;
}
}
public static void MapReturnType(EntryHandler handler, IEntry entry)
{
// Return type affects keyboard on mobile; on desktop, Enter always completes
// ReturnType affects keyboard on mobile; access PlatformView to ensure it exists
_ = handler.PlatformView;
}
public static void MapIsEnabled(EntryHandler handler, IEntry entry)
public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry)
{
handler.PlatformView.IsEnabled = entry.IsEnabled;
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
// ClearButtonVisibility.WhileEditing = 1
handler.PlatformView.ShowClearButton = (int)entry.ClearButtonVisibility == 1;
}
}
public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView != null)
{
handler.PlatformView.HorizontalTextAlignment = (int)entry.HorizontalTextAlignment switch
{
0 => TextAlignment.Start,
1 => TextAlignment.Center,
2 => TextAlignment.End,
_ => TextAlignment.Start
};
}
}
public static void MapVerticalTextAlignment(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView != null)
{
handler.PlatformView.VerticalTextAlignment = (int)entry.VerticalTextAlignment switch
{
0 => TextAlignment.Start,
1 => TextAlignment.Center,
2 => TextAlignment.End,
_ => TextAlignment.Center
};
}
}
public static void MapBackground(EntryHandler handler, IEntry entry)
{
var background = entry.Background;
if (background is SolidColorBrush solidBrush && solidBrush.Color != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
if (entry.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
handler.PlatformView.Invalidate();
}
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
{
if (entry is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (handler.PlatformView == null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
return;
}
if (entry is Entry mauiEntry)
{
Console.WriteLine($"[EntryHandler] MapBackgroundColor: {mauiEntry.BackgroundColor}");
if (mauiEntry.BackgroundColor != null)
{
var color = mauiEntry.BackgroundColor.ToSKColor();
Console.WriteLine($"[EntryHandler] Setting EntryBackgroundColor to: {color}");
handler.PlatformView.EntryBackgroundColor = color;
}
}
}
}

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;
@@ -31,6 +32,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -212,4 +214,17 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
{
if (handler.PlatformView is null) return;
if (entry is Entry ve && ve.BackgroundColor != null)
{
Console.WriteLine($"[EntryHandler] MapBackgroundColor: {ve.BackgroundColor}");
var color = ve.BackgroundColor.ToSKColor();
Console.WriteLine($"[EntryHandler] Setting EntryBackgroundColor to: {color}");
handler.PlatformView.EntryBackgroundColor = color;
}
}
}

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.SpaceAround,
_ => FlexAlignContent.Stretch,
};
}
}
}

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,6 +38,31 @@ 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)
@@ -92,7 +118,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)

563
Handlers/GestureManager.cs Normal file
View File

@@ -0,0 +1,563 @@
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, 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; }
}
private enum PointerEventType
{
Entered,
Exited,
Pressed,
Moved,
Released
}
private static MethodInfo? _sendTappedMethod;
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;
/// <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>
/// 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;
}
}

View File

@@ -0,0 +1,233 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.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;
}
Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready");
}
protected override void DisconnectHandler(GtkWebViewProxy platformView)
{
if (_platformWebView != null)
{
_platformWebView.NavigationStarted -= OnNavigationStarted;
_platformWebView.NavigationCompleted -= OnNavigationCompleted;
UnregisterFromHost();
_platformWebView.Dispose();
_platformWebView = null;
}
base.DisconnectHandler(platformView);
}
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,164 @@
using System;
using Microsoft.Maui.Platform.Linux.Native;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <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 WebKitNative.LoadChangedCallback? _loadChangedCallback;
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 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);
Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget");
}
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;
if (_widget != IntPtr.Zero)
{
WebKitNative.DisconnectLoadChanged(_widget);
}
_widget = IntPtr.Zero;
_loadChangedCallback = null;
}
}
}

View File

@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Platform;
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(SKRect bounds)
{
base.Arrange(bounds);
var windowBounds = TransformToWindow(bounds);
_handler.RegisterWithHost(windowBounds);
}
private SKRect TransformToWindow(SKRect localBounds)
{
float x = localBounds.Left;
float y = localBounds.Top;
for (var parent = Parent; parent != null; parent = parent.Parent)
{
x += parent.Bounds.Left;
y += parent.Bounds.Top;
}
return new SKRect(x, y, x + localBounds.Width, y + localBounds.Height);
}
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(Bounds, 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,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius,
[nameof(IPadding.Padding)] = MapPadding,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
};
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -154,6 +155,16 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
}
}
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.BackgroundColor = imgBtn.BackgroundColor.ToSKColor();
}
}
// Image source loading helper
private ImageSourceServiceResultManager _sourceLoader = null!;

View File

@@ -0,0 +1,308 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Threading;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Image control.
/// </summary>
public class ImageHandler : ViewHandler<IImage, SkiaImage>
{
internal class ImageSourceServiceResultManager
{
private readonly ImageHandler _handler;
private CancellationTokenSource? _cts;
public ImageSourceServiceResultManager(ImageHandler handler)
{
_handler = handler;
}
public async void UpdateImageSourceAsync()
{
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
var source = _handler.VirtualView?.Source;
if (source == null)
{
_handler.PlatformView?.LoadFromData(Array.Empty<byte>());
return;
}
if (_handler.VirtualView is IImageSourcePart imagePart)
{
imagePart.UpdateIsLoading(true);
}
if (source is IFileImageSource fileSource)
{
var file = fileSource.File;
if (!string.IsNullOrEmpty(file))
{
await _handler.PlatformView.LoadFromFileAsync(file);
}
return;
}
if (source is IUriImageSource uriSource)
{
var uri = uriSource.Uri;
if (uri != null)
{
await _handler.PlatformView.LoadFromUriAsync(uri);
}
return;
}
if (source is IStreamImageSource streamSource)
{
var stream = await streamSource.GetStreamAsync(token);
if (stream != null)
{
await _handler.PlatformView.LoadFromStreamAsync(stream);
}
return;
}
if (source is FontImageSource fontSource)
{
var bitmap = RenderFontImageSource(fontSource, _handler.PlatformView.WidthRequest, _handler.PlatformView.HeightRequest);
if (bitmap != null)
{
_handler.PlatformView.LoadFromBitmap(bitmap);
}
}
}
catch (OperationCanceledException)
{
// Cancelled - ignore
}
catch (Exception)
{
if (_handler.VirtualView is IImageSourcePart imagePart)
{
imagePart.UpdateIsLoading(false);
}
}
}
private static SKBitmap? RenderFontImageSource(FontImageSource fontSource, double requestedWidth, double requestedHeight)
{
var 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);
var color = fontSource.Color?.ToSKColor() ?? SKColors.Black;
var bitmap = new SKBitmap(size, size, false);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
SKTypeface? typeface = null;
if (!string.IsNullOrEmpty(fontSource.FontFamily))
{
var fontPaths = new[]
{
"/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 (var path in fontPaths)
{
if (File.Exists(path))
{
typeface = SKTypeface.FromFile(path);
if (typeface != null)
break;
}
}
if (typeface == null)
{
typeface = SKTypeface.FromFamilyName(fontSource.FontFamily);
}
}
typeface ??= SKTypeface.Default;
float fontSize = size * 0.8f;
using var font = new SKFont(typeface, fontSize);
using var paint = new SKPaint(font)
{
Color = color,
IsAntialias = true,
TextAlign = SKTextAlign.Center
};
var bounds = new SKRect();
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;
}
}
public static IPropertyMapper<IImage, ImageHandler> Mapper = new PropertyMapper<IImage, ImageHandler>(ViewHandler.ViewMapper)
{
["Aspect"] = MapAspect,
["IsOpaque"] = MapIsOpaque,
["Source"] = MapSource,
["Background"] = MapBackground,
["Width"] = MapWidth,
["Height"] = MapHeight
};
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
private ImageSourceServiceResultManager? _sourceLoader;
private ImageSourceServiceResultManager SourceLoader => _sourceLoader ??= new ImageSourceServiceResultManager(this);
public ImageHandler() : base(Mapper, CommandMapper)
{
}
public ImageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaImage CreatePlatformView()
{
return new SkiaImage();
}
protected override void ConnectHandler(SkiaImage platformView)
{
base.ConnectHandler(platformView);
platformView.ImageLoaded += OnImageLoaded;
platformView.ImageLoadingError += OnImageLoadingError;
}
protected override void DisconnectHandler(SkiaImage platformView)
{
platformView.ImageLoaded -= OnImageLoaded;
platformView.ImageLoadingError -= OnImageLoadingError;
base.DisconnectHandler(platformView);
}
private void OnImageLoaded(object? sender, EventArgs e)
{
if (VirtualView is IImageSourcePart imagePart)
{
imagePart.UpdateIsLoading(false);
}
}
private void OnImageLoadingError(object? sender, ImageLoadingErrorEventArgs e)
{
if (VirtualView is IImageSourcePart imagePart)
{
imagePart.UpdateIsLoading(false);
}
}
public static void MapAspect(ImageHandler handler, IImage image)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Aspect = image.Aspect;
}
}
public static void MapIsOpaque(ImageHandler handler, IImage image)
{
if (handler.PlatformView != null)
{
handler.PlatformView.IsOpaque = image.IsOpaque;
}
}
public static void MapSource(ImageHandler handler, IImage image)
{
if (handler.PlatformView == null)
{
return;
}
if (image is Image mauiImage)
{
if (mauiImage.WidthRequest > 0)
{
handler.PlatformView.WidthRequest = mauiImage.WidthRequest;
}
if (mauiImage.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = mauiImage.HeightRequest;
}
}
handler.SourceLoader.UpdateImageSourceAsync();
}
public static void MapBackground(ImageHandler handler, IImage image)
{
if (handler.PlatformView != null)
{
if (image.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapWidth(ImageHandler handler, IImage image)
{
if (handler.PlatformView != null)
{
if (image is Image mauiImage && mauiImage.WidthRequest > 0)
{
handler.PlatformView.WidthRequest = mauiImage.WidthRequest;
Console.WriteLine($"[ImageHandler] MapWidth: {mauiImage.WidthRequest}");
}
else if (image.Width > 0)
{
handler.PlatformView.WidthRequest = image.Width;
}
}
}
public static void MapHeight(ImageHandler handler, IImage image)
{
if (handler.PlatformView != null)
{
if (image is Image mauiImage && mauiImage.HeightRequest > 0)
{
handler.PlatformView.HeightRequest = mauiImage.HeightRequest;
Console.WriteLine($"[ImageHandler] MapHeight: {mauiImage.HeightRequest}");
}
else if (image.Height > 0)
{
handler.PlatformView.HeightRequest = image.Height;
}
}
}
}

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,8 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
[nameof(IImage.IsOpaque)] = MapIsOpaque,
[nameof(IImageSourcePart.Source)] = MapSource,
[nameof(IView.Background)] = MapBackground,
["Width"] = MapWidth,
["Height"] = MapHeight,
};
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -88,6 +92,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();
}
@@ -101,6 +118,36 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
}
}
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;
}
}
// Image source loading helper
private ImageSourceServiceResultManager _sourceLoader = null!;
@@ -162,6 +209,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 +231,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

@@ -1,52 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Primitives;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Label control.
/// </summary>
public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
public 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,
["Text"] = MapText,
["TextColor"] = MapTextColor,
["Font"] = MapFont,
["CharacterSpacing"] = MapCharacterSpacing,
["HorizontalTextAlignment"] = MapHorizontalTextAlignment,
["VerticalTextAlignment"] = MapVerticalTextAlignment,
["TextDecorations"] = MapTextDecorations,
["LineHeight"] = MapLineHeight,
["LineBreakMode"] = MapLineBreakMode,
["MaxLines"] = MapMaxLines,
["Padding"] = MapPadding,
["Background"] = MapBackground,
["VerticalLayoutAlignment"] = MapVerticalLayoutAlignment,
["HorizontalLayoutAlignment"] = MapHorizontalLayoutAlignment,
["FormattedText"] = MapFormattedText
};
/// <summary>
/// Maps the command mapper for the handler.
/// </summary>
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
};
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)
public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
@@ -56,119 +53,263 @@ 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;
if (view.GestureRecognizers.OfType<TapGestureRecognizer>().Any())
{
platformView.CursorType = CursorType.Hand;
}
}
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, 0.0);
}
}
public static void MapText(LabelHandler handler, ILabel label)
{
handler.PlatformView.Text = label.Text ?? "";
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
handler.PlatformView.Text = label.Text ?? string.Empty;
}
}
public static void MapTextColor(LabelHandler handler, ILabel label)
{
if (label.TextColor != null)
if (handler.PlatformView != null && 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)
if (handler.PlatformView != null)
{
handler.PlatformView.FontFamily = font.Family;
var font = label.Font;
if (font.Size > 0)
{
handler.PlatformView.FontSize = (float)font.Size;
}
if (!string.IsNullOrEmpty(font.Family))
{
handler.PlatformView.FontFamily = font.Family;
}
handler.PlatformView.IsBold = (int)font.Weight >= 700;
handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique;
}
}
public static void MapCharacterSpacing(LabelHandler handler, ILabel label)
{
if (handler.PlatformView != null)
{
handler.PlatformView.CharacterSpacing = (float)label.CharacterSpacing;
}
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
if (handler.PlatformView != null)
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
handler.PlatformView.Invalidate();
handler.PlatformView.HorizontalTextAlignment = (int)label.HorizontalTextAlignment switch
{
0 => TextAlignment.Start,
1 => TextAlignment.Center,
2 => TextAlignment.End,
_ => TextAlignment.Start
};
}
}
public static void MapVerticalTextAlignment(LabelHandler handler, ILabel label)
{
handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch
if (handler.PlatformView != null)
{
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();
handler.PlatformView.VerticalTextAlignment = (int)label.VerticalTextAlignment switch
{
0 => TextAlignment.Start,
1 => TextAlignment.Center,
2 => TextAlignment.End,
_ => TextAlignment.Center
};
}
}
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();
if (handler.PlatformView != null)
{
handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0;
handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0;
}
}
public static void MapLineHeight(LabelHandler handler, ILabel label)
{
handler.PlatformView.LineHeight = (float)label.LineHeight;
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
handler.PlatformView.LineHeight = (float)label.LineHeight;
}
}
public static void MapLineBreakMode(LabelHandler handler, ILabel label)
{
if (handler.PlatformView != null)
{
if (label is Label mauiLabel)
{
handler.PlatformView.LineBreakMode = (int)mauiLabel.LineBreakMode switch
{
0 => LineBreakMode.NoWrap,
1 => LineBreakMode.WordWrap,
2 => LineBreakMode.CharacterWrap,
3 => LineBreakMode.HeadTruncation,
4 => LineBreakMode.TailTruncation,
5 => LineBreakMode.MiddleTruncation,
_ => LineBreakMode.TailTruncation
};
}
}
}
public static void MapMaxLines(LabelHandler handler, ILabel label)
{
if (handler.PlatformView != null)
{
if (label is Label mauiLabel)
{
handler.PlatformView.MaxLines = mauiLabel.MaxLines;
}
}
}
public static void MapPadding(LabelHandler handler, ILabel label)
{
if (handler.PlatformView != null)
{
var padding = label.Padding;
handler.PlatformView.Padding = new SKRect(
(float)padding.Left,
(float)padding.Top,
(float)padding.Right,
(float)padding.Bottom);
}
}
public static void MapBackground(LabelHandler handler, ILabel label)
{
if (label.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
if (label.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapBackgroundColor(LabelHandler handler, ILabel label)
public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label)
{
if (label is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
handler.PlatformView.VerticalOptions = (int)label.VerticalLayoutAlignment switch
{
1 => LayoutOptions.Start,
2 => LayoutOptions.Center,
3 => LayoutOptions.End,
0 => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}
public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label)
{
if (handler.PlatformView != null)
{
handler.PlatformView.HorizontalOptions = (int)label.HorizontalLayoutAlignment switch
{
1 => LayoutOptions.Start,
2 => LayoutOptions.Center,
3 => LayoutOptions.End,
0 => LayoutOptions.Fill,
_ => LayoutOptions.Start
};
}
}
public static void MapFormattedText(LabelHandler handler, ILabel label)
{
if (handler.PlatformView == null)
{
return;
}
if (label is not Label mauiLabel)
{
handler.PlatformView.FormattedSpans = null;
return;
}
var formattedText = mauiLabel.FormattedText;
if (formattedText == null || formattedText.Spans.Count == 0)
{
handler.PlatformView.FormattedSpans = null;
return;
}
var spans = new List<SkiaTextSpan>();
foreach (var span in formattedText.Spans)
{
var skiaSpan = new SkiaTextSpan
{
Text = span.Text ?? "",
IsBold = span.FontAttributes.HasFlag(FontAttributes.Bold),
IsItalic = span.FontAttributes.HasFlag(FontAttributes.Italic),
IsUnderline = (span.TextDecorations & TextDecorations.Underline) != 0,
IsStrikethrough = (span.TextDecorations & TextDecorations.Strikethrough) != 0,
CharacterSpacing = (float)span.CharacterSpacing,
LineHeight = (float)span.LineHeight
};
if (span.TextColor != null)
{
skiaSpan.TextColor = span.TextColor.ToSKColor();
}
if (span.BackgroundColor != null)
{
skiaSpan.BackgroundColor = span.BackgroundColor.ToSKColor();
}
if (!string.IsNullOrEmpty(span.FontFamily))
{
skiaSpan.FontFamily = span.FontFamily;
}
if (span.FontSize > 0)
{
skiaSpan.FontSize = (float)span.FontSize;
}
spans.Add(skiaSpan);
}
handler.PlatformView.FormattedSpans = spans;
}
}

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,39 @@ 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;
}
}
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;
@@ -205,4 +242,53 @@ 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.FormattedSpans = null;
return;
}
var formattedText = mauiLabel.FormattedText;
if (formattedText == null || formattedText.Spans.Count == 0)
{
handler.PlatformView.FormattedSpans = null;
return;
}
var spans = new List<SkiaTextSpan>();
foreach (var span in formattedText.Spans)
{
var skiaSpan = new SkiaTextSpan
{
Text = span.Text ?? "",
IsBold = span.FontAttributes.HasFlag(FontAttributes.Bold),
IsItalic = span.FontAttributes.HasFlag(FontAttributes.Italic),
IsUnderline = (span.TextDecorations & TextDecorations.Underline) != 0,
IsStrikethrough = (span.TextDecorations & TextDecorations.Strikethrough) != 0,
CharacterSpacing = (float)span.CharacterSpacing,
LineHeight = (float)span.LineHeight
};
if (span.TextColor != null)
skiaSpan.TextColor = span.TextColor.ToSKColor();
if (span.BackgroundColor != null)
skiaSpan.BackgroundColor = span.BackgroundColor.ToSKColor();
if (!string.IsNullOrEmpty(span.FontFamily))
skiaSpan.FontFamily = span.FontFamily;
if (span.FontSize > 0)
skiaSpan.FontSize = (float)span.FontSize;
spans.Add(skiaSpan);
}
handler.PlatformView.FormattedSpans = spans;
}
}

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;
@@ -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)
@@ -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;
@@ -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
@@ -284,7 +285,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

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)
@@ -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;
@@ -21,6 +22,7 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
[nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource,
[nameof(Page.Padding)] = MapPadding,
[nameof(IView.Background)] = MapBackground,
[nameof(VisualElement.BackgroundColor)] = MapBackgroundColor,
};
public static CommandMapper<Page, PageHandler> CommandMapper =
@@ -100,6 +102,18 @@ public partial class PageHandler : ViewHandler<Page, SkiaPage>
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
}
}
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.ToSKColor();
Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}");
}
}
}
/// <summary>
@@ -144,7 +158,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

View File

@@ -0,0 +1,178 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Specialized;
using System.Linq;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Picker control.
/// </summary>
public class PickerHandler : ViewHandler<IPicker, SkiaPicker>
{
public static IPropertyMapper<IPicker, PickerHandler> Mapper = new PropertyMapper<IPicker, PickerHandler>(ViewHandler.ViewMapper)
{
["Title"] = MapTitle,
["TitleColor"] = MapTitleColor,
["SelectedIndex"] = MapSelectedIndex,
["TextColor"] = MapTextColor,
["Font"] = MapFont,
["CharacterSpacing"] = MapCharacterSpacing,
["HorizontalTextAlignment"] = MapHorizontalTextAlignment,
["VerticalTextAlignment"] = MapVerticalTextAlignment,
["Background"] = MapBackground,
["ItemsSource"] = MapItemsSource
};
public static CommandMapper<IPicker, PickerHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
private INotifyCollectionChanged? _itemsCollection;
public PickerHandler() : base(Mapper, CommandMapper)
{
}
public PickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaPicker CreatePlatformView()
{
return new SkiaPicker();
}
protected override void ConnectHandler(SkiaPicker platformView)
{
base.ConnectHandler(platformView);
platformView.SelectedIndexChanged += OnSelectedIndexChanged;
if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged itemsCollection)
{
_itemsCollection = itemsCollection;
_itemsCollection.CollectionChanged += OnItemsCollectionChanged;
}
ReloadItems();
}
protected override void DisconnectHandler(SkiaPicker platformView)
{
platformView.SelectedIndexChanged -= OnSelectedIndexChanged;
if (_itemsCollection != null)
{
_itemsCollection.CollectionChanged -= OnItemsCollectionChanged;
_itemsCollection = null;
}
base.DisconnectHandler(platformView);
}
private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ReloadItems();
}
private void OnSelectedIndexChanged(object? sender, EventArgs e)
{
if (VirtualView != null && PlatformView != null)
{
VirtualView.SelectedIndex = PlatformView.SelectedIndex;
}
}
private void ReloadItems()
{
if (PlatformView != null && VirtualView != null)
{
var items = VirtualView.GetItemsAsArray();
PlatformView.SetItems(items.Select(i => i?.ToString() ?? ""));
}
}
public static void MapTitle(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Title = picker.Title ?? "";
}
}
public static void MapTitleColor(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null && picker.TitleColor != null)
{
handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor();
}
}
public static void MapSelectedIndex(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null)
{
handler.PlatformView.SelectedIndex = picker.SelectedIndex;
}
}
public static void MapTextColor(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null && picker.TextColor != null)
{
handler.PlatformView.TextColor = picker.TextColor.ToSKColor();
}
}
public static void MapFont(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null)
{
var font = picker.Font;
if (!string.IsNullOrEmpty(font.Family))
{
handler.PlatformView.FontFamily = font.Family;
}
if (font.Size > 0)
{
handler.PlatformView.FontSize = (float)font.Size;
}
handler.PlatformView.Invalidate();
}
}
public static void MapCharacterSpacing(PickerHandler handler, IPicker picker)
{
// Character spacing not implemented
}
public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker)
{
// Horizontal text alignment not implemented
}
public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker)
{
// Vertical text alignment not implemented
}
public static void MapBackground(PickerHandler handler, IPicker picker)
{
if (handler.PlatformView != null)
{
if (picker.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapItemsSource(PickerHandler handler, IPicker picker)
{
handler.ReloadItems();
}
}

View File

@@ -22,6 +22,7 @@ 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,
@@ -129,6 +130,22 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
}
}
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 = (float)font.Size;
}
handler.PlatformView.Invalidate();
}
public static void MapCharacterSpacing(PickerHandler handler, IPicker picker)
{
// Character spacing could be implemented with custom text rendering

View File

@@ -1,29 +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 System.ComponentModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for ProgressBar control.
/// </summary>
public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar>
public 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,
["Progress"] = MapProgress,
["ProgressColor"] = MapProgressColor,
["IsEnabled"] = MapIsEnabled,
["Background"] = MapBackground,
["BackgroundColor"] = MapBackgroundColor
};
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public ProgressBarHandler() : base(Mapper, CommandMapper) { }
public ProgressBarHandler() : base(Mapper, CommandMapper)
{
}
protected override SkiaProgressBar CreatePlatformView() => new SkiaProgressBar();
protected override SkiaProgressBar CreatePlatformView()
{
return new SkiaProgressBar();
}
protected override void ConnectHandler(SkiaProgressBar platformView)
{
base.ConnectHandler(platformView);
if (VirtualView is BindableObject bindable)
{
bindable.PropertyChanged += OnVirtualViewPropertyChanged;
}
if (VirtualView is VisualElement ve)
{
platformView.IsVisible = ve.IsVisible;
}
}
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 ve && e.PropertyName == "IsVisible")
{
PlatformView.IsVisible = ve.IsVisible;
PlatformView.Invalidate();
}
}
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
{
@@ -33,7 +75,9 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
public static void MapProgressColor(ProgressBarHandler handler, IProgress progress)
{
if (progress.ProgressColor != null)
{
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
}
handler.PlatformView.Invalidate();
}
@@ -45,16 +89,16 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
{
if (progress.Background is SolidColorBrush solidBrush && solidBrush.Color != null)
if (progress.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress)
{
if (progress is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (progress is VisualElement ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();

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.ComponentModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using SkiaSharp;
@@ -18,7 +20,9 @@ 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,
};
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -39,6 +43,40 @@ 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;
}
}
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 +88,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.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)
@@ -60,6 +109,18 @@ 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.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.BackgroundColor = visualElement.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
}

View File

@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for RadioButton control.
/// </summary>
public class RadioButtonHandler : ViewHandler<IRadioButton, SkiaRadioButton>
{
public static IPropertyMapper<IRadioButton, RadioButtonHandler> Mapper = new PropertyMapper<IRadioButton, RadioButtonHandler>(ViewHandler.ViewMapper)
{
["IsChecked"] = MapIsChecked,
["TextColor"] = MapTextColor,
["Font"] = MapFont,
["Background"] = MapBackground
};
public static CommandMapper<IRadioButton, RadioButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public RadioButtonHandler() : base(Mapper, CommandMapper)
{
}
public RadioButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaRadioButton CreatePlatformView()
{
return new SkiaRadioButton();
}
protected override void ConnectHandler(SkiaRadioButton platformView)
{
base.ConnectHandler(platformView);
platformView.CheckedChanged += OnCheckedChanged;
if (VirtualView is RadioButton radioButton)
{
platformView.Content = radioButton.Content?.ToString() ?? "";
platformView.GroupName = radioButton.GroupName;
platformView.Value = radioButton.Value;
}
}
protected override void DisconnectHandler(SkiaRadioButton platformView)
{
platformView.CheckedChanged -= OnCheckedChanged;
base.DisconnectHandler(platformView);
}
private void OnCheckedChanged(object? sender, EventArgs e)
{
if (VirtualView != null && PlatformView != null)
{
VirtualView.IsChecked = PlatformView.IsChecked;
}
}
public static void MapIsChecked(RadioButtonHandler handler, IRadioButton radioButton)
{
if (handler.PlatformView != null)
{
handler.PlatformView.IsChecked = radioButton.IsChecked;
}
}
public static void MapTextColor(RadioButtonHandler handler, IRadioButton radioButton)
{
if (handler.PlatformView != null && radioButton.TextColor != null)
{
handler.PlatformView.TextColor = radioButton.TextColor.ToSKColor();
}
}
public static void MapFont(RadioButtonHandler handler, IRadioButton radioButton)
{
if (handler.PlatformView != null)
{
var font = radioButton.Font;
if (font.Size > 0)
{
handler.PlatformView.FontSize = (float)font.Size;
}
}
}
public static void MapBackground(RadioButtonHandler handler, IRadioButton radioButton)
{
if (handler.PlatformView != null)
{
if (radioButton.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
}

View File

@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for ScrollView control.
/// </summary>
public class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView>
{
public static IPropertyMapper<IScrollView, ScrollViewHandler> Mapper = new PropertyMapper<IScrollView, ScrollViewHandler>(ViewHandler.ViewMapper)
{
["Content"] = MapContent,
["HorizontalScrollBarVisibility"] = MapHorizontalScrollBarVisibility,
["VerticalScrollBarVisibility"] = MapVerticalScrollBarVisibility,
["Orientation"] = MapOrientation
};
public static CommandMapper<IScrollView, ScrollViewHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
{
["RequestScrollTo"] = MapRequestScrollTo
};
public ScrollViewHandler() : base(Mapper, CommandMapper)
{
}
public ScrollViewHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
protected override SkiaScrollView CreatePlatformView()
{
return new SkiaScrollView();
}
public static void MapContent(ScrollViewHandler handler, IScrollView scrollView)
{
if (handler.PlatformView == null || handler.MauiContext == null)
{
return;
}
var presentedContent = scrollView.PresentedContent;
if (presentedContent != null)
{
Console.WriteLine("[ScrollViewHandler] MapContent: " + presentedContent.GetType().Name);
if (presentedContent.Handler == null)
{
presentedContent.Handler = presentedContent.ToViewHandler(handler.MauiContext);
}
if (presentedContent.Handler?.PlatformView is SkiaView skiaView)
{
Console.WriteLine("[ScrollViewHandler] Setting content: " + skiaView.GetType().Name);
handler.PlatformView.Content = skiaView;
}
}
else
{
handler.PlatformView.Content = null;
}
}
public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.HorizontalScrollBarVisibility = (int)scrollView.HorizontalScrollBarVisibility switch
{
1 => ScrollBarVisibility.Always,
2 => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.VerticalScrollBarVisibility = (int)scrollView.VerticalScrollBarVisibility switch
{
1 => ScrollBarVisibility.Always,
2 => ScrollBarVisibility.Never,
_ => ScrollBarVisibility.Default
};
}
public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView)
{
handler.PlatformView.Orientation = ((int)scrollView.Orientation - 1) switch
{
0 => ScrollOrientation.Horizontal,
1 => ScrollOrientation.Both,
2 => ScrollOrientation.Neither,
_ => ScrollOrientation.Vertical
};
}
public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args)
{
if (args is ScrollToRequest request)
{
handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant);
}
}
}

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,31 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for SearchBar control.
/// </summary>
public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
public 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,
["Text"] = MapText,
["TextColor"] = MapTextColor,
["Font"] = MapFont,
["Placeholder"] = MapPlaceholder,
["PlaceholderColor"] = MapPlaceholderColor,
["CancelButtonColor"] = MapCancelButtonColor,
["Background"] = MapBackground
};
public static CommandMapper<ISearchBar, SearchBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SearchBarHandler() : base(Mapper, CommandMapper) { }
public SearchBarHandler() : base(Mapper, CommandMapper)
{
}
protected override SkiaSearchBar CreatePlatformView() => new SkiaSearchBar();
public SearchBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaSearchBar CreatePlatformView()
{
return new SkiaSearchBar();
}
protected override void ConnectHandler(SkiaSearchBar platformView)
{
@@ -43,9 +55,9 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (VirtualView != null && VirtualView.Text != e.NewText)
if (VirtualView != null && PlatformView != null && VirtualView.Text != e.NewTextValue)
{
VirtualView.Text = e.NewText;
VirtualView.Text = e.NewTextValue ?? string.Empty;
}
}
@@ -56,51 +68,68 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
public static void MapText(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView.Text != searchBar.Text)
if (handler.PlatformView != null && handler.PlatformView.Text != searchBar.Text)
{
handler.PlatformView.Text = searchBar.Text ?? "";
handler.PlatformView.Text = searchBar.Text ?? string.Empty;
}
}
public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView != null && searchBar.TextColor != null)
{
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
}
}
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
{
if (handler.PlatformView != null)
{
var font = searchBar.Font;
if (font.Size > 0)
{
handler.PlatformView.FontSize = (float)font.Size;
}
if (!string.IsNullOrEmpty(font.Family))
{
handler.PlatformView.FontFamily = font.Family;
}
}
}
public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar)
{
handler.PlatformView.Placeholder = searchBar.Placeholder ?? "";
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
handler.PlatformView.Placeholder = searchBar.Placeholder ?? string.Empty;
}
}
public static void MapPlaceholderColor(SearchBarHandler handler, ISearchBar searchBar)
{
if (searchBar.PlaceholderColor != null)
if (handler.PlatformView != null && searchBar.PlaceholderColor != null)
{
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar)
public static void MapCancelButtonColor(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();
if (handler.PlatformView != null && searchBar.CancelButtonColor != null)
{
handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor.ToSKColor();
}
}
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();
if (handler.PlatformView != null)
{
if (searchBar.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
}

View File

@@ -1,33 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Slider control.
/// </summary>
public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
public 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,
["Minimum"] = MapMinimum,
["Maximum"] = MapMaximum,
["Value"] = MapValue,
["MinimumTrackColor"] = MapMinimumTrackColor,
["MaximumTrackColor"] = MapMaximumTrackColor,
["ThumbColor"] = MapThumbColor,
["Background"] = MapBackground,
["IsEnabled"] = MapIsEnabled
};
public static CommandMapper<ISlider, SliderHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SliderHandler() : base(Mapper, CommandMapper) { }
public SliderHandler() : base(Mapper, CommandMapper)
{
}
protected override SkiaSlider CreatePlatformView() => new SkiaSlider();
public SliderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaSlider CreatePlatformView()
{
return new SkiaSlider();
}
protected override void ConnectHandler(SkiaSlider platformView)
{
@@ -35,6 +46,14 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
platformView.ValueChanged += OnValueChanged;
platformView.DragStarted += OnDragStarted;
platformView.DragCompleted += OnDragCompleted;
if (VirtualView != null)
{
MapMinimum(this, VirtualView);
MapMaximum(this, VirtualView);
MapValue(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
protected override void DisconnectHandler(SkiaSlider platformView)
@@ -47,30 +66,41 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
private void OnValueChanged(object? sender, SliderValueChangedEventArgs e)
{
if (VirtualView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.001)
if (VirtualView != null && PlatformView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.0001)
{
VirtualView.Value = e.NewValue;
}
}
private void OnDragStarted(object? sender, EventArgs e) => VirtualView?.DragStarted();
private void OnDragCompleted(object? sender, EventArgs e) => VirtualView?.DragCompleted();
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();
if (handler.PlatformView != null)
{
handler.PlatformView.Minimum = slider.Minimum;
}
}
public static void MapMaximum(SliderHandler handler, ISlider slider)
{
handler.PlatformView.Maximum = slider.Maximum;
handler.PlatformView.Invalidate();
if (handler.PlatformView != null)
{
handler.PlatformView.Maximum = slider.Maximum;
}
}
public static void MapValue(SliderHandler handler, ISlider slider)
{
if (Math.Abs(handler.PlatformView.Value - slider.Value) > 0.001)
if (handler.PlatformView != null && Math.Abs(handler.PlatformView.Value - slider.Value) > 0.0001)
{
handler.PlatformView.Value = slider.Value;
}
@@ -78,45 +108,44 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MinimumTrackColor != null)
if (handler.PlatformView != null && slider.MinimumTrackColor != null)
{
handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{
if (slider.MaximumTrackColor != null)
if (handler.PlatformView != null && slider.MaximumTrackColor != null)
{
handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor();
handler.PlatformView.Invalidate();
}
}
public static void MapThumbColor(SliderHandler handler, ISlider slider)
{
if (slider.ThumbColor != null)
if (handler.PlatformView != null && 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)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
if (slider.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapBackgroundColor(SliderHandler handler, ISlider slider)
public static void MapIsEnabled(SliderHandler handler, ISlider slider)
{
if (slider is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.IsEnabled = slider.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -0,0 +1,127 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Stepper control.
/// </summary>
public class StepperHandler : ViewHandler<IStepper, SkiaStepper>
{
public static IPropertyMapper<IStepper, StepperHandler> Mapper = new PropertyMapper<IStepper, StepperHandler>(ViewHandler.ViewMapper)
{
["Value"] = MapValue,
["Minimum"] = MapMinimum,
["Maximum"] = MapMaximum,
["Increment"] = MapIncrement,
["Background"] = MapBackground,
["IsEnabled"] = MapIsEnabled
};
public static CommandMapper<IStepper, StepperHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public StepperHandler() : base(Mapper, CommandMapper)
{
}
public StepperHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaStepper CreatePlatformView()
{
return new SkiaStepper();
}
protected override void ConnectHandler(SkiaStepper platformView)
{
base.ConnectHandler(platformView);
platformView.ValueChanged += OnValueChanged;
// Apply dark theme colors if needed
if (Application.Current?.UserAppTheme == AppTheme.Dark)
{
platformView.ButtonBackgroundColor = new SKColor(66, 66, 66);
platformView.ButtonPressedColor = new SKColor(97, 97, 97);
platformView.ButtonDisabledColor = new SKColor(48, 48, 48);
platformView.SymbolColor = new SKColor(224, 224, 224);
platformView.SymbolDisabledColor = new SKColor(97, 97, 97);
platformView.BorderColor = new SKColor(97, 97, 97);
}
}
protected override void DisconnectHandler(SkiaStepper platformView)
{
platformView.ValueChanged -= OnValueChanged;
base.DisconnectHandler(platformView);
}
private void OnValueChanged(object? sender, EventArgs e)
{
if (VirtualView != null && PlatformView != null)
{
VirtualView.Value = PlatformView.Value;
}
}
public static void MapValue(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Value = stepper.Value;
}
}
public static void MapMinimum(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Minimum = stepper.Minimum;
}
}
public static void MapMaximum(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
handler.PlatformView.Maximum = stepper.Maximum;
}
}
public static void MapBackground(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
if (stepper.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapIncrement(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
if (stepper is Stepper mauiStepper)
{
handler.PlatformView.Increment = mauiStepper.Increment;
}
}
}
public static void MapIsEnabled(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView != null)
{
handler.PlatformView.IsEnabled = stepper.IsEnabled;
}
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using SkiaSharp;
@@ -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,17 @@ 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 = new SKColor(66, 66, 66);
platformView.ButtonPressedColor = new SKColor(97, 97, 97);
platformView.ButtonDisabledColor = new SKColor(48, 48, 48);
platformView.SymbolColor = new SKColor(224, 224, 224);
platformView.SymbolDisabledColor = new SKColor(97, 97, 97);
platformView.BorderColor = new SKColor(97, 97, 97);
}
}
protected override void DisconnectHandler(SkiaStepper platformView)
@@ -86,4 +100,20 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
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;
}
}

View File

@@ -1,30 +1,41 @@
// 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.Handlers;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Linux handler for Switch control.
/// </summary>
public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
public 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,
["IsOn"] = MapIsOn,
["TrackColor"] = MapTrackColor,
["ThumbColor"] = MapThumbColor,
["Background"] = MapBackground,
["IsEnabled"] = MapIsEnabled
};
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper);
public SwitchHandler() : base(Mapper, CommandMapper) { }
public SwitchHandler() : base(Mapper, CommandMapper)
{
}
protected override SkiaSwitch CreatePlatformView() => new SkiaSwitch();
public SwitchHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
protected override SkiaSwitch CreatePlatformView()
{
return new SkiaSwitch();
}
protected override void ConnectHandler(SkiaSwitch platformView)
{
@@ -48,7 +59,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
public static void MapIsOn(SwitchHandler handler, ISwitch @switch)
{
if (handler.PlatformView.IsOn != @switch.IsOn)
if (handler.PlatformView != null)
{
handler.PlatformView.IsOn = @switch.IsOn;
}
@@ -56,39 +67,38 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
public static void MapTrackColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.TrackColor != null)
handler.PlatformView.OnTrackColor = @switch.TrackColor.ToSKColor();
handler.PlatformView.Invalidate();
if (handler.PlatformView != null && @switch.TrackColor != null)
{
var onTrackColor = @switch.TrackColor.ToSKColor();
handler.PlatformView.OnTrackColor = onTrackColor;
handler.PlatformView.OffTrackColor = onTrackColor.WithAlpha(128);
}
}
public static void MapThumbColor(SwitchHandler handler, ISwitch @switch)
{
if (@switch.ThumbColor != null)
if (handler.PlatformView != null && @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)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor();
handler.PlatformView.Invalidate();
if (@switch.Background is SolidPaint solidPaint && solidPaint.Color != null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
}
public static void MapBackgroundColor(SwitchHandler handler, ISwitch @switch)
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
{
if (@switch is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null)
if (handler.PlatformView != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.Invalidate();
handler.PlatformView.IsEnabled = @switch.IsEnabled;
}
}
}

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)
@@ -96,4 +97,10 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
}
}
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = @switch.IsEnabled;
}
}

View File

@@ -47,6 +47,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 = new SKColor(30, 30, 30);
platformView.ClockFaceColor = new SKColor(45, 45, 45);
platformView.TextColor = new SKColor(224, 224, 224);
platformView.BorderColor = new SKColor(97, 97, 97);
platformView.BackgroundColor = new SKColor(45, 45, 45);
}
}
protected override void DisconnectHandler(SkiaTimePicker platformView)

View File

@@ -54,29 +54,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)

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,9 @@ public static class LinuxMauiAppBuilderExtensions
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
builder.Services.TryAddSingleton<IEmail, EmailService>();
// Register GTK host service
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
// Register type converters for XAML support
RegisterTypeConverters();
@@ -98,8 +105,8 @@ public static class LinuxMauiAppBuilderExtensions
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
// Web
handlers.AddHandler<WebView, WebViewHandler>();
// Web - use GtkWebViewHandler
handlers.AddHandler<WebView, GtkWebViewHandler>();
// Collection Views
handlers.AddHandler<CollectionView, CollectionViewHandler>();
@@ -124,33 +131,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,10 @@
// 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.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
@@ -44,6 +45,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);

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;
@@ -198,9 +200,28 @@ 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);
}
}
// Set flyout footer with version info
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 +231,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 +248,61 @@ 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)
{
var color = shell.FlyoutBackgroundColor;
skiaShell.FlyoutBackgroundColor = new SKColor(
(byte)(color.Red * 255f),
(byte)(color.Green * 255f),
(byte)(color.Blue * 255f),
(byte)(color.Alpha * 255f));
Console.WriteLine($"[ApplyShellColors] FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
}
else
{
skiaShell.FlyoutBackgroundColor = isDark
? new SKColor(30, 30, 30)
: new SKColor(255, 255, 255);
Console.WriteLine($"[ApplyShellColors] Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
}
// Flyout text color
skiaShell.FlyoutTextColor = isDark
? new SKColor(224, 224, 224)
: new SKColor(33, 33, 33);
Console.WriteLine($"[ApplyShellColors] FlyoutTextColor: {skiaShell.FlyoutTextColor}");
// Content background color
skiaShell.ContentBackgroundColor = isDark
? new SKColor(18, 18, 18)
: new SKColor(250, 250, 250);
Console.WriteLine($"[ApplyShellColors] ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
// NavBar background color
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
{
var color = shell.BackgroundColor;
skiaShell.NavBarBackgroundColor = new SKColor(
(byte)(color.Red * 255f),
(byte)(color.Green * 255f),
(byte)(color.Blue * 255f),
(byte)(color.Alpha * 255f));
}
else
{
skiaShell.NavBarBackgroundColor = new SKColor(33, 150, 243); // Material blue
}
}
/// <summary>
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
/// </summary>
@@ -290,7 +370,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 +409,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 +441,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 +485,38 @@ public class LinuxViewRenderer
var contentView = RenderView(cp.Content);
if (contentView != null)
{
if (contentView is SkiaScrollView)
// Get page background color if set
SKColor? bgColor = null;
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
{
return contentView;
var color = cp.BackgroundColor;
bgColor = new SKColor(
(byte)(color.Red * 255f),
(byte)(color.Green * 255f),
(byte)(color.Blue * 255f),
(byte)(color.Alpha * 255f));
Console.WriteLine($"[CreateShellContentPage] Page BackgroundColor: {bgColor}");
}
if (contentView is SkiaScrollView scrollView)
{
if (bgColor.HasValue)
{
scrollView.BackgroundColor = bgColor.Value;
}
return scrollView;
}
else
{
var scrollView = new SkiaScrollView
var newScrollView = new SkiaScrollView
{
Content = contentView
};
return scrollView;
if (bgColor.HasValue)
{
newScrollView.BackgroundColor = bgColor.Value;
}
return newScrollView;
}
}
}
@@ -476,22 +580,3 @@ public class LinuxViewRenderer
}
}
/// <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;
}

226
Interop/X11.cs Normal file
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 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;
[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 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);
[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;
}

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

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

10
Interop/XWindowClass.cs Normal file
View File

@@ -0,0 +1,10 @@
// 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 XWindowClass
{
public const uint InputOutput = 1;
public const uint InputOnly = 2;
}

View File

@@ -1,12 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux.Dispatching;
using Microsoft.Maui.Platform.Linux.Hosting;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux;
@@ -15,19 +27,114 @@ namespace Microsoft.Maui.Platform.Linux;
/// </summary>
public class LinuxApplication : IDisposable
{
private static int _invalidateCount;
private static int _requestRedrawCount;
private static int _drawCount;
private static int _gtkThreadId;
private static DateTime _lastCounterReset = DateTime.Now;
private static bool _isRedrawing;
private static int _loopCounter = 0;
private X11Window? _mainWindow;
private GtkHostWindow? _gtkWindow;
private SkiaRenderingEngine? _renderingEngine;
private SkiaView? _rootView;
private SkiaView? _focusedView;
private SkiaView? _hoveredView;
private SkiaView? _capturedView; // View that has captured pointer events during drag
private bool _disposed;
private bool _useGtk;
/// <summary>
/// Gets the current application instance.
/// </summary>
public static LinuxApplication? Current { get; private set; }
/// <summary>
/// Gets whether the application is running in GTK mode.
/// </summary>
public static bool IsGtkMode => Current?._useGtk ?? false;
/// <summary>
/// Logs an invalidate call for diagnostics.
/// </summary>
public static void LogInvalidate(string source)
{
int currentThread = Environment.CurrentManagedThreadId;
Interlocked.Increment(ref _invalidateCount);
if (currentThread != _gtkThreadId && _gtkThreadId != 0)
{
Console.WriteLine($"[DIAG] ⚠️ Invalidate from WRONG THREAD! GTK={_gtkThreadId}, Current={currentThread}, Source={source}");
}
}
/// <summary>
/// Logs a request redraw call for diagnostics.
/// </summary>
public static void LogRequestRedraw()
{
int currentThread = Environment.CurrentManagedThreadId;
Interlocked.Increment(ref _requestRedrawCount);
if (currentThread != _gtkThreadId && _gtkThreadId != 0)
{
Console.WriteLine($"[DIAG] ⚠️ RequestRedraw from WRONG THREAD! GTK={_gtkThreadId}, Current={currentThread}");
}
}
private static void StartHeartbeat()
{
_gtkThreadId = Environment.CurrentManagedThreadId;
Console.WriteLine($"[DIAG] GTK thread ID: {_gtkThreadId}");
GLibNative.TimeoutAdd(250, () =>
{
DateTime now = DateTime.Now;
if ((now - _lastCounterReset).TotalSeconds >= 1.0)
{
int invalidates = Interlocked.Exchange(ref _invalidateCount, 0);
int redraws = Interlocked.Exchange(ref _requestRedrawCount, 0);
int draws = Interlocked.Exchange(ref _drawCount, 0);
Console.WriteLine($"[DIAG] ❤️ Heartbeat | Invalidate={invalidates}/s, RequestRedraw={redraws}/s, Draw={draws}/s");
_lastCounterReset = now;
}
return true;
});
}
/// <summary>
/// Logs a draw call for diagnostics.
/// </summary>
public static void LogDraw()
{
Interlocked.Increment(ref _drawCount);
}
/// <summary>
/// Requests a redraw of the application.
/// </summary>
public static void RequestRedraw()
{
LogRequestRedraw();
if (_isRedrawing)
return;
_isRedrawing = true;
try
{
if (Current != null && Current._useGtk)
{
Current._gtkWindow?.RequestRedraw();
}
else
{
Current?._renderingEngine?.InvalidateAll();
}
}
finally
{
_isRedrawing = false;
}
}
/// <summary>
/// Gets the main window.
/// </summary>
@@ -112,84 +219,99 @@ public class LinuxApplication : IDisposable
/// <param name="configure">Optional configuration action.</param>
public static void Run(MauiApp app, string[] args, Action<LinuxApplicationOptions>? configure)
{
// Initialize dispatcher
LinuxDispatcher.Initialize();
DispatcherProvider.SetCurrent(LinuxDispatcherProvider.Instance);
Console.WriteLine("[LinuxApplication] Dispatcher initialized");
var options = app.Services.GetService<LinuxApplicationOptions>()
?? new LinuxApplicationOptions();
configure?.Invoke(options);
ParseCommandLineOptions(args, options);
using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options);
// Create MAUI context
var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp);
// Get the application and render it
var application = app.Services.GetService<IApplication>();
SkiaView? rootView = null;
if (application is Microsoft.Maui.Controls.Application mauiApplication)
var linuxApp = new LinuxApplication();
try
{
// Force Application.Current to be this instance
// The constructor sets Current = this, but we ensure it here
var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current");
if (currentProperty != null && currentProperty.CanWrite)
linuxApp.Initialize(options);
// Create MAUI context
var mauiContext = new LinuxMauiContext(app.Services, linuxApp);
// Get the application and render it
var application = app.Services.GetService<IApplication>();
SkiaView? rootView = null;
if (application is Application mauiApplication)
{
currentProperty.SetValue(null, mauiApplication);
// Force Application.Current to be this instance
var currentProperty = typeof(Application).GetProperty("Current");
if (currentProperty != null && currentProperty.CanWrite)
{
currentProperty.SetValue(null, mauiApplication);
}
// Handle theme changes
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
{
if (e.PropertyName == "UserAppTheme")
{
Console.WriteLine($"[LinuxApplication] Theme changed to: {mauiApplication.UserAppTheme}");
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
linuxApp._renderingEngine?.InvalidateAll();
}
};
if (mauiApplication.MainPage != null)
{
var mainPage = mauiApplication.MainPage;
var windowsField = typeof(Application).GetField("_windows",
BindingFlags.NonPublic | BindingFlags.Instance);
var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
if (windowsList != null && windowsList.Count == 0)
{
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
windowsList.Add(mauiWindow);
mauiWindow.Parent = mauiApplication;
}
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
{
windowsList[0].Page = mainPage;
}
var renderer = new LinuxViewRenderer(mauiContext);
rootView = renderer.RenderPage(mainPage);
string windowTitle = "OpenMaui App";
if (mainPage is NavigationPage navPage)
{
windowTitle = navPage.Title ?? windowTitle;
}
else if (mainPage is Shell shell)
{
windowTitle = shell.Title ?? windowTitle;
}
else
{
windowTitle = mainPage.Title ?? windowTitle;
}
linuxApp.SetWindowTitle(windowTitle);
}
}
if (mauiApplication.MainPage != null)
if (rootView == null)
{
// Create a MAUI Window and add it to the application
// This ensures Shell.Current works (it reads from Application.Current.Windows[0].Page)
var mainPage = mauiApplication.MainPage;
// Always ensure we have a window with the Shell/Page
var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List<Microsoft.Maui.Controls.Window>;
if (windowsList != null && windowsList.Count == 0)
{
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
windowsList.Add(mauiWindow);
mauiWindow.Parent = mauiApplication;
}
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
{
// Window exists but has no page - set it
windowsList[0].Page = mainPage;
}
var renderer = new Hosting.LinuxViewRenderer(mauiContext);
rootView = renderer.RenderPage(mainPage);
// Update window title based on app name (NavigationPage.Title takes precedence)
string windowTitle = "OpenMaui App";
if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage)
{
// Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title
windowTitle = navPage.Title ?? windowTitle;
}
else if (mainPage is Microsoft.Maui.Controls.Shell shell)
{
windowTitle = shell.Title ?? windowTitle;
}
else
{
windowTitle = mainPage.Title ?? windowTitle;
}
linuxApp.SetWindowTitle(windowTitle);
rootView = LinuxProgramHost.CreateDemoView();
}
}
// Fallback to demo if no view
if (rootView == null)
linuxApp.RootView = rootView;
linuxApp.Run();
}
finally
{
rootView = Hosting.LinuxProgramHost.CreateDemoView();
linuxApp?.Dispose();
}
linuxApp.RootView = rootView;
linuxApp.Run();
}
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
@@ -218,16 +340,37 @@ public class LinuxApplication : IDisposable
/// </summary>
public void Initialize(LinuxApplicationOptions options)
{
// Create the main window
_useGtk = options.UseGtk;
if (_useGtk)
{
InitializeGtk(options);
}
else
{
InitializeX11(options);
}
RegisterServices();
}
private void InitializeX11(LinuxApplicationOptions options)
{
_mainWindow = new X11Window(
options.Title ?? "MAUI Application",
options.Width,
options.Height);
// Create the rendering engine
// Set up WebView main window
SkiaWebView.SetMainWindow(_mainWindow.Display, _mainWindow.Handle);
// Set window icon
string? iconPath = ResolveIconPath(options.IconPath);
if (!string.IsNullOrEmpty(iconPath))
{
_mainWindow.SetIcon(iconPath);
}
_renderingEngine = new SkiaRenderingEngine(_mainWindow);
// Wire up events
_mainWindow.Resized += OnWindowResized;
_mainWindow.Exposed += OnWindowExposed;
_mainWindow.KeyDown += OnKeyDown;
@@ -238,9 +381,69 @@ public class LinuxApplication : IDisposable
_mainWindow.PointerReleased += OnPointerReleased;
_mainWindow.Scroll += OnScroll;
_mainWindow.CloseRequested += OnCloseRequested;
}
// Register platform services
RegisterServices();
private void InitializeGtk(LinuxApplicationOptions options)
{
_gtkWindow = GtkHostService.Instance.GetOrCreateHostWindow(
options.Title ?? "MAUI Application",
options.Width,
options.Height);
string? iconPath = ResolveIconPath(options.IconPath);
if (!string.IsNullOrEmpty(iconPath))
{
GtkHostService.Instance.SetWindowIcon(iconPath);
}
if (_gtkWindow.SkiaSurface != null)
{
_gtkWindow.SkiaSurface.DrawRequested += OnGtkDrawRequested;
_gtkWindow.SkiaSurface.PointerPressed += OnGtkPointerPressed;
_gtkWindow.SkiaSurface.PointerReleased += OnGtkPointerReleased;
_gtkWindow.SkiaSurface.PointerMoved += OnGtkPointerMoved;
_gtkWindow.SkiaSurface.KeyPressed += OnGtkKeyPressed;
_gtkWindow.SkiaSurface.KeyReleased += OnGtkKeyReleased;
_gtkWindow.SkiaSurface.Scrolled += OnGtkScrolled;
_gtkWindow.SkiaSurface.TextInput += OnGtkTextInput;
}
_gtkWindow.Resized += OnGtkResized;
}
private static string? ResolveIconPath(string? explicitPath)
{
if (!string.IsNullOrEmpty(explicitPath))
{
if (Path.IsPathRooted(explicitPath))
{
return File.Exists(explicitPath) ? explicitPath : null;
}
string resolved = Path.Combine(AppContext.BaseDirectory, explicitPath);
return File.Exists(resolved) ? resolved : null;
}
string baseDir = AppContext.BaseDirectory;
// Check for appicon.meta (generated icon)
string metaPath = Path.Combine(baseDir, "appicon.meta");
if (File.Exists(metaPath))
{
string? generated = MauiIconGenerator.GenerateIcon(metaPath);
if (!string.IsNullOrEmpty(generated) && File.Exists(generated))
{
return generated;
}
}
// Check for appicon.png
string pngPath = Path.Combine(baseDir, "appicon.png");
if (File.Exists(pngPath)) return pngPath;
// Check for appicon.svg
string svgPath = Path.Combine(baseDir, "appicon.svg");
if (File.Exists(svgPath)) return svgPath;
return null;
}
private void RegisterServices()
@@ -261,27 +464,62 @@ public class LinuxApplication : IDisposable
/// Shows the main window and runs the event loop.
/// </summary>
public void Run()
{
if (_useGtk)
{
RunGtk();
}
else
{
RunX11();
}
}
private void RunX11()
{
if (_mainWindow == null)
throw new InvalidOperationException("Application not initialized");
_mainWindow.Show();
// Initial render
Render();
// Run the event loop
Console.WriteLine("[LinuxApplication] Starting event loop");
while (_mainWindow.IsRunning)
{
_mainWindow.ProcessEvents();
_loopCounter++;
if (_loopCounter % 1000 == 0)
{
Console.WriteLine($"[LinuxApplication] Loop iteration {_loopCounter}");
}
// Update animations and render
_mainWindow.ProcessEvents();
SkiaWebView.ProcessGtkEvents();
UpdateAnimations();
Render();
// Small delay to prevent 100% CPU usage
Thread.Sleep(1);
}
Console.WriteLine("[LinuxApplication] Event loop ended");
}
private void RunGtk()
{
if (_gtkWindow == null)
throw new InvalidOperationException("Application not initialized");
StartHeartbeat();
PerformGtkLayout(_gtkWindow.Width, _gtkWindow.Height);
_gtkWindow.RequestRedraw();
_gtkWindow.Run();
GtkHostService.Instance.Shutdown();
}
private void PerformGtkLayout(int width, int height)
{
if (_rootView != null)
{
_rootView.Measure(new SKSize(width, height));
_rootView.Arrange(new SKRect(0, 0, width, height));
}
}
private void UpdateAnimations()
@@ -358,6 +596,13 @@ public class LinuxApplication : IDisposable
private void OnPointerMoved(object? sender, PointerEventArgs e)
{
// Route to context menu if one is active
if (LinuxDialogService.HasContextMenu)
{
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e);
return;
}
// Route to dialog if one is active
if (LinuxDialogService.HasActiveDialog)
{
@@ -384,6 +629,10 @@ public class LinuxApplication : IDisposable
_hoveredView?.OnPointerExited(e);
_hoveredView = hitView;
_hoveredView?.OnPointerEntered(e);
// Update cursor based on view's cursor type
CursorType cursor = hitView?.CursorType ?? CursorType.Arrow;
_mainWindow?.SetCursor(cursor);
}
hitView?.OnPointerMoved(e);
@@ -394,6 +643,13 @@ public class LinuxApplication : IDisposable
{
Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})");
// Route to context menu if one is active
if (LinuxDialogService.HasContextMenu)
{
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(e);
return;
}
// Route to dialog if one is active
if (LinuxDialogService.HasActiveDialog)
{
@@ -489,6 +745,224 @@ public class LinuxApplication : IDisposable
_mainWindow?.Stop();
}
// GTK Event Handlers
private void OnGtkDrawRequested(object? sender, EventArgs e)
{
Console.WriteLine("[DIAG] >>> OnGtkDrawRequested ENTER");
LogDraw();
var surface = _gtkWindow?.SkiaSurface;
if (surface?.Canvas != null && _rootView != null)
{
var bgColor = Application.Current?.UserAppTheme == AppTheme.Dark
? new SKColor(32, 33, 36)
: SKColors.White;
surface.Canvas.Clear(bgColor);
Console.WriteLine("[DIAG] Drawing rootView...");
_rootView.Draw(surface.Canvas);
Console.WriteLine("[DIAG] Drawing dialogs...");
var bounds = new SKRect(0, 0, surface.Width, surface.Height);
LinuxDialogService.DrawDialogs(surface.Canvas, bounds);
Console.WriteLine("[DIAG] <<< OnGtkDrawRequested EXIT");
}
}
private void OnGtkResized(object? sender, (int Width, int Height) size)
{
PerformGtkLayout(size.Width, size.Height);
_gtkWindow?.RequestRedraw();
}
private void OnGtkPointerPressed(object? sender, (double X, double Y, int Button) e)
{
string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})";
Console.WriteLine($"[LinuxApplication.GTK] PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
if (LinuxDialogService.HasContextMenu)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(args);
_gtkWindow?.RequestRedraw();
return;
}
if (_rootView == null)
{
Console.WriteLine("[LinuxApplication.GTK] _rootView is null!");
return;
}
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
Console.WriteLine($"[LinuxApplication.GTK] HitView: {hitView?.GetType().Name ?? "null"}");
if (hitView != null)
{
if (hitView.IsFocusable && _focusedView != hitView)
{
_focusedView?.OnFocusLost();
_focusedView = hitView;
_focusedView.OnFocusGained();
}
_capturedView = hitView;
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
Console.WriteLine("[DIAG] >>> Before OnPointerPressed");
hitView.OnPointerPressed(args);
Console.WriteLine("[DIAG] <<< After OnPointerPressed, calling RequestRedraw");
_gtkWindow?.RequestRedraw();
Console.WriteLine("[DIAG] <<< After RequestRedraw, returning from handler");
}
}
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
{
Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER");
if (_rootView == null) return;
if (_capturedView != null)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
Console.WriteLine($"[DIAG] Calling OnPointerReleased on {_capturedView.GetType().Name}");
_capturedView.OnPointerReleased(args);
Console.WriteLine("[DIAG] OnPointerReleased returned");
_capturedView = null;
_gtkWindow?.RequestRedraw();
Console.WriteLine("[DIAG] <<< OnGtkPointerReleased EXIT (captured path)");
}
else
{
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
if (hitView != null)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
hitView.OnPointerReleased(args);
_gtkWindow?.RequestRedraw();
}
}
}
private void OnGtkPointerMoved(object? sender, (double X, double Y) e)
{
if (LinuxDialogService.HasContextMenu)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(args);
_gtkWindow?.RequestRedraw();
return;
}
if (_rootView == null) return;
if (_capturedView != null)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
_capturedView.OnPointerMoved(args);
_gtkWindow?.RequestRedraw();
return;
}
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
if (hitView != _hoveredView)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
_hoveredView?.OnPointerExited(args);
_hoveredView = hitView;
_hoveredView?.OnPointerEntered(args);
_gtkWindow?.RequestRedraw();
}
if (hitView != null)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
hitView.OnPointerMoved(args);
}
}
private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
{
if (_focusedView != null)
{
var key = ConvertGdkKey(e.KeyVal);
var modifiers = ConvertGdkModifiers(e.State);
var args = new KeyEventArgs(key, modifiers);
_focusedView.OnKeyDown(args);
_gtkWindow?.RequestRedraw();
}
}
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
{
if (_focusedView != null)
{
var key = ConvertGdkKey(e.KeyVal);
var modifiers = ConvertGdkModifiers(e.State);
var args = new KeyEventArgs(key, modifiers);
_focusedView.OnKeyUp(args);
_gtkWindow?.RequestRedraw();
}
}
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY) e)
{
if (_rootView == null) return;
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
while (hitView != null)
{
if (hitView is SkiaScrollView scrollView)
{
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY);
scrollView.OnScroll(args);
_gtkWindow?.RequestRedraw();
break;
}
hitView = hitView.Parent;
}
}
private void OnGtkTextInput(object? sender, string text)
{
if (_focusedView != null)
{
var args = new TextInputEventArgs(text);
_focusedView.OnTextInput(args);
_gtkWindow?.RequestRedraw();
}
}
private static Key ConvertGdkKey(uint keyval)
{
return keyval switch
{
65288 => Key.Backspace,
65289 => Key.Tab,
65293 => Key.Enter,
65307 => Key.Escape,
65360 => Key.Home,
65361 => Key.Left,
65362 => Key.Up,
65363 => Key.Right,
65364 => Key.Down,
65365 => Key.PageUp,
65366 => Key.PageDown,
65367 => Key.End,
65535 => Key.Delete,
>= 32 and <= 126 => (Key)keyval,
_ => Key.Unknown
};
}
private static KeyModifiers ConvertGdkModifiers(uint state)
{
var modifiers = KeyModifiers.None;
if ((state & 1) != 0) modifiers |= KeyModifiers.Shift;
if ((state & 4) != 0) modifiers |= KeyModifiers.Control;
if ((state & 8) != 0) modifiers |= KeyModifiers.Alt;
return modifiers;
}
public void Dispose()
{
if (!_disposed)
@@ -503,60 +977,3 @@ public class LinuxApplication : IDisposable
}
}
}
/// <summary>
/// Options for Linux application initialization.
/// </summary>
public class LinuxApplicationOptions
{
/// <summary>
/// Gets or sets the window title.
/// </summary>
public string? Title { get; set; } = "MAUI Application";
/// <summary>
/// Gets or sets the initial window width.
/// </summary>
public int Width { get; set; } = 800;
/// <summary>
/// Gets or sets the initial window height.
/// </summary>
public int Height { get; set; } = 600;
/// <summary>
/// Gets or sets whether to use hardware acceleration.
/// </summary>
public bool UseHardwareAcceleration { get; set; } = true;
/// <summary>
/// Gets or sets the display server type.
/// </summary>
public DisplayServerType DisplayServer { get; set; } = DisplayServerType.Auto;
/// <summary>
/// Gets or sets whether to force demo mode instead of loading the application's pages.
/// </summary>
public bool ForceDemo { get; set; } = false;
}
/// <summary>
/// Display server type options.
/// </summary>
public enum DisplayServerType
{
/// <summary>
/// Automatically detect the display server.
/// </summary>
Auto,
/// <summary>
/// Use X11 (Xorg).
/// </summary>
X11,
/// <summary>
/// Use Wayland.
/// </summary>
Wayland
}

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;
public class LinuxApplicationOptions
{
public string? Title { get; set; } = "MAUI Application";
public int Width { get; set; } = 800;
public int Height { get; set; } = 600;
public bool UseHardwareAcceleration { get; set; } = true;
public DisplayServerType DisplayServer { get; set; }
public bool ForceDemo { get; set; }
public string? IconPath { get; set; }
public bool UseGtk { get; set; }
}

354
MERGE_TRACKING.md Normal file
View File

@@ -0,0 +1,354 @@
# OpenMaui Linux - Recovery Merge Tracking
**Branch:** `final`
**Last Updated:** 2026-01-01
**Build Status:** SUCCEEDS
---
## HANDLERS
**CRITICAL**: All handlers must use namespace `Microsoft.Maui.Platform.Linux.Handlers` and follow decompiled EXACTLY.
| File | Status | Notes |
|------|--------|-------|
| ActivityIndicatorHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed IsEnabled/BackgroundColor (not in production), fixed namespace |
| ApplicationHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| BorderHandler.cs | [x] | **FIXED 2026-01-01** - Added ConnectHandler/DisconnectHandler with MauiView and Tapped event, OnPlatformViewTapped calls GestureManager.ProcessTap |
| BoxViewHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, Color/CornerRadius/Background/BackgroundColor |
| ButtonHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed MapText/TextColor/Font (not in production), fixed namespace, added null checks |
| CheckBoxHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added VerticalLayoutAlignment/HorizontalLayoutAlignment, fixed namespace |
| CollectionViewHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| DatePickerHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, dark theme support |
| EditorHandler.Linux.cs | [x] | **CREATED 2026-01-01** - Was missing, created from decompiled |
| EntryHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added CharacterSpacing/ClearButtonVisibility/VerticalTextAlignment, fixed namespace, null checks |
| FlexLayoutHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| FlyoutPageHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| FrameHandler.cs | [x] | **FIXED 2026-01-01** - Added ConnectHandler/DisconnectHandler with MauiView and Tapped event, OnPlatformViewTapped calls GestureManager.ProcessTap |
| GestureManager.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| GraphicsViewHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| GtkWebViewHandler.cs | [x] | Added new file from decompiled |
| GtkWebViewManager.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| GtkWebViewPlatformView.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| GtkWebViewProxy.cs | [x] | Added new file from decompiled |
| ImageButtonHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, has ImageSourceServiceResultManager |
| ImageHandler.Linux.cs | [x] | **VERIFIED 2026-01-01** - Matches production, FontImageSource rendering |
| ItemsViewHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| LabelHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added CharacterSpacing/LayoutAlignment/FormattedText, ConnectHandler gesture logic, fixed namespace |
| LayoutHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, includes StackLayoutHandler/GridHandler |
| NavigationPageHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, toolbar items, SVG/PNG icons |
| PageHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, includes ContentPageHandler |
| PickerHandler.Linux.cs | [x] | **CREATED 2026-01-01** - Was missing, created from decompiled with collection changed tracking |
| ProgressBarHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added ConnectHandler/DisconnectHandler IsVisible tracking, fixed namespace |
| RadioButtonHandler.Linux.cs | [x] | **VERIFIED 2026-01-01** - Matches production, Content/GroupName/Value in ConnectHandler |
| ScrollViewHandler.Linux.cs | [x] | **VERIFIED 2026-01-01** - Matches production, CommandMapper with RequestScrollTo |
| SearchBarHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Fixed namespace, added CancelButtonColor, SolidPaint, null checks |
| ShellHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, navigation event handling |
| SliderHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed BackgroundColor (use base), fixed namespace, added ConnectHandler init calls |
| StepperHandler.Linux.cs | [x] | **VERIFIED 2026-01-01** - Matches production, dark theme support |
| SwitchHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added OffTrackColor logic, fixed namespace, removed extra BackgroundColor |
| TabbedPageHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, SelectedIndexChanged event |
| TimePickerHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, dark theme support |
| WebViewHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production |
| WindowHandler.cs | [x] | **VERIFIED 2026-01-01** - Matches production, includes SkiaWindow class |
---
## VIEWS
| File | Status | Notes |
|------|--------|-------|
| SkiaActivityIndicator.cs | [x] | Verified - all TwoWay, logic matches |
| SkiaAlertDialog.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, modal dialog rendering |
| SkiaBorder.cs | [x] | **FIXED 2026-01-01** - Logic matches, removed embedded SkiaFrame (now separate file) |
| SkiaFrame.cs | [x] | **ADDED 2026-01-01** - Created as separate file matching decompiled pattern |
| SkiaBoxView.cs | [x] | Verified - all TwoWay, logic matches |
| SkiaButton.cs | [x] | Verified - all TwoWay, logic matches |
| SkiaCarouselView.cs | [x] | **FIXED 2026-01-01** - Logic matches, removed embedded PositionChangedEventArgs |
| PositionChangedEventArgs.cs | [x] | **ADDED 2026-01-01** - Created as separate file matching decompiled |
| SkiaCheckBox.cs | [x] | Verified - IsChecked=OneWay, rest TwoWay, logic matches |
| SkiaCollectionView.cs | [x] | **FIXED 2026-01-01** - Removed embedded SkiaSelectionMode, ItemsLayoutOrientation |
| SkiaSelectionMode.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| ItemsLayoutOrientation.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaContentPresenter.cs | [x] | **FIXED 2026-01-01** - Removed embedded LayoutAlignment (now separate file) |
| LayoutAlignment.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaContextMenu.cs | [x] | **VERIFIED 2026-01-01** - Logic matches decompiled |
| SkiaDatePicker.cs | [x] | **VERIFIED 2026-01-01** - Date=OneWay, all others=TwoWay |
| SkiaEditor.cs | [x] | **FIXED 2026-01-01** - All BindingModes corrected (Text=OneWay, others=TwoWay) |
| SkiaEntry.cs | [x] | **FIXED 2026-01-01** - TextProperty BindingMode.OneWay, others TwoWay |
| SkiaFlexLayout.cs | [x] | **VERIFIED 2026-01-01** - Logic matches decompiled |
| SkiaFlyoutPage.cs | [x] | **FIXED 2026-01-01** - Removed embedded FlyoutLayoutBehavior (now separate file) |
| FlyoutLayoutBehavior.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaGraphicsView.cs | [x] | **VERIFIED 2026-01-01** - Logic matches decompiled |
| SkiaImage.cs | [x] | **VERIFIED 2026-01-01** - No BindableProperties, logic matches |
| SkiaImageButton.cs | [x] | **FIXED 2026-01-01** - Added SVG support, multi-path search matching decompiled |
| SkiaIndicatorView.cs | [x] | **FIXED 2026-01-01** - Removed embedded IndicatorShape (now separate file) |
| IndicatorShape.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaItemsView.cs | [x] | Added GetItemView() method |
| SkiaLabel.cs | [x] | **FIXED 2026-01-01** - All BindingModes TwoWay |
| SkiaLayoutView.cs | [x] | **FIXED 2026-01-01** - All BindingModes TwoWay (Spacing, Padding, ClipToBounds, Orientation, RowSpacing, ColumnSpacing) |
| SkiaMenuBar.cs | [x] | **FIXED 2026-01-01** - Removed embedded MenuBarItem, MenuItem, SkiaMenuFlyout, MenuItemClickedEventArgs |
| MenuBarItem.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| MenuItem.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaMenuFlyout.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| MenuItemClickedEventArgs.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaNavigationPage.cs | [x] | **FIXED 2026-01-01** - Added LinuxApplication.IsGtkMode check, removed embedded NavigationEventArgs |
| NavigationEventArgs.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaPage.cs | [x] | Added SkiaToolbarItem.Icon property |
| SkiaPicker.cs | [x] | FIXED - SelectedIndex=OneWay, all others=TwoWay (was missing) |
| SkiaProgressBar.cs | [x] | Verified - Progress=OneWay, rest TwoWay, logic matches |
| SkiaRadioButton.cs | [x] | **FIXED 2026-01-01** - IsChecked=OneWay, all others=TwoWay |
| SkiaRefreshView.cs | [x] | **FIXED 2026-01-01** - Added ICommand support (Command, CommandParameter) matching decompiled |
| SkiaScrollView.cs | [x] | **FIXED 2026-01-01** - All BindingModes TwoWay |
| SkiaSearchBar.cs | [x] | **VERIFIED 2026-01-01** - No BindableProperties, logic matches |
| SkiaShell.cs | [x] | **FIXED 2026-01-01** - Added FlyoutTextColor, ContentBackgroundColor, route registration, query parameters, OnScroll |
| SkiaSlider.cs | [x] | FIXED - Value=OneWay, rest TwoWay (agent had inverted all) |
| SkiaStepper.cs | [x] | **FIXED 2026-01-01** - Value=OneWay, all others=TwoWay |
| SkiaSwipeView.cs | [x] | **FIXED 2026-01-01** - Removed embedded SwipeItem, SwipeDirection, SwipeMode, SwipeStartedEventArgs, SwipeEndedEventArgs |
| SwipeItem.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SwipeDirection.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SwipeMode.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SwipeStartedEventArgs.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SwipeEndedEventArgs.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaSwitch.cs | [x] | FIXED - IsOn=OneWay (agent had TwoWay) |
| SkiaTabbedPage.cs | [x] | **FIXED 2026-01-01** - Removed embedded TabItem (now separate file) |
| TabItem.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaTemplatedView.cs | [x] | **FIXED 2026-01-01** - Added missing using statements (Shapes, Graphics) |
| SkiaTimePicker.cs | [x] | **FIXED 2026-01-01** - Time=OneWay, all others=TwoWay |
| SkiaView.cs | [x] | Made Arrange() virtual |
| SkiaVisualStateManager.cs | [x] | **FIXED 2026-01-01** - Removed embedded SkiaVisualStateGroupList, SkiaVisualStateGroup, SkiaVisualState, SkiaVisualStateSetter |
| SkiaVisualStateGroupList.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaVisualStateGroup.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaVisualState.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaVisualStateSetter.cs | [x] | **ADDED 2026-01-01** - Created as separate file |
| SkiaWebView.cs | [x] | **FIXED 2026-01-01** - Full X11 embedding, position tracking, hardware accel, load callbacks |
---
## SERVICES
| File | Status | Notes |
|------|--------|-------|
| AccessibilityServiceFactory.cs | [x] | **FIXED 2026-01-01** - Fixed CreateService() to call Initialize(), added Reset() |
| AccessibleAction.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| AccessibleProperty.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (Name,Description,Role,Value,Parent,Children) |
| AccessibleRect.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| AccessibleRole.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (simplified list with Button,Tab,TabPanel) |
| AccessibleState.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| AccessibleStates.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (MultiSelectable capital S) |
| AnnouncementPriority.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (Polite,Assertive) |
| AppActionsService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean string interpolation |
| AppInfoService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean enum names |
| AtSpi2AccessibilityService.cs | [x] | **FIXED 2026-01-01** - Removed embedded AccessibilityServiceFactory, NullAccessibilityService |
| BrowserService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean nameof/interpolation/enums |
| ClipboardService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, xclip/xsel fallback |
| ColorDialogResult.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| ConnectivityService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean enum names |
| DesktopEnvironment.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (GNOME uppercase) |
| DeviceDisplayService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean enum names |
| DeviceInfoService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean enum names |
| DisplayServerFactory.cs | [x] | **FIXED 2026-01-01** - Removed embedded DisplayServerType, IDisplayWindow, X11DisplayWindow, WaylandDisplayWindow |
| DisplayServerType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| DragAction.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| DragData.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| DragDropService.cs | [x] | **FIXED 2026-01-01** - Removed embedded DragData, DragEventArgs, DropEventArgs, DragAction |
| DragEventArgs.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| DropEventArgs.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| EmailService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean nameof/interpolation |
| Fcitx5InputMethodService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, D-Bus interface |
| FileDialogResult.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| FilePickerService.cs | [x] | **FIXED 2026-01-01** - Removed embedded LinuxFileResult |
| FolderPickerOptions.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| FolderPickerResult.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| FolderPickerService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, zenity/kdialog fallback |
| FolderResult.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled |
| FontFallbackManager.cs | [x] | **FIXED 2026-01-01** - Removed embedded TextRun |
| GlobalHotkeyService.cs | [x] | **FIXED 2026-01-01** - Removed embedded HotkeyEventArgs, HotkeyModifiers, HotkeyKey |
| Gtk4InteropService.cs | [x] | **FIXED 2026-01-01** - Removed embedded GtkResponseType, GtkMessageType, GtkButtonsType, GtkFileChooserAction, FileDialogResult, ColorDialogResult |
| GtkButtonsType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| GtkContextMenuService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, GTK P/Invoke |
| GtkFileChooserAction.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| GtkHostService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, main has clean ??= syntax |
| GtkMenuItem.cs | [x] | **VERIFIED 2026-01-01** - Identical logic |
| GtkMessageType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| GtkResponseType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| HardwareVideoService.cs | [x] | **FIXED 2026-01-01** - Removed embedded VideoAccelerationApi, VideoProfile, VideoFrame |
| HiDpiService.cs | [x] | **FIXED 2026-01-01** - Removed embedded ScaleChangedEventArgs |
| HighContrastChangedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| HighContrastColors.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| HighContrastService.cs | [x] | **FIXED 2026-01-01** - Removed embedded HighContrastTheme, HighContrastColors, HighContrastChangedEventArgs |
| HighContrastTheme.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled (None,WhiteOnBlack,BlackOnWhite) |
| HotkeyEventArgs.cs | [x] | **FIXED 2026-01-01** - Fixed constructor order (int id, HotkeyKey key, HotkeyModifiers modifiers) |
| HotkeyKey.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| HotkeyModifiers.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| IAccessibilityService.cs | [x] | **FIXED 2026-01-01** - Removed many embedded types |
| IAccessible.cs | [x] | **FIXED 2026-01-01** - Fixed to match decompiled exactly |
| IAccessibleEditableText.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| IAccessibleText.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| IBusInputMethodService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, IBus D-Bus interface |
| IDisplayWindow.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| IInputContext.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| IInputMethodService.cs | [x] | **FIXED 2026-01-01** - Removed embedded IInputContext, TextCommittedEventArgs, PreEditChangedEventArgs, PreEditAttribute, PreEditAttributeType, KeyModifiers |
| InputMethodServiceFactory.cs | [x] | **FIXED 2026-01-01** - Removed embedded NullInputMethodService |
| KeyModifiers.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| LauncherService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, xdg-open |
| LinuxFileResult.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| LinuxResourcesProvider.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, system styles |
| MauiIconGenerator.cs | [x] | **FIXED 2026-01-01** - Added Svg.Skia, SVG foreground, Scale metadata |
| NotificationAction.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationActionEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationClosedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationCloseReason.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationContext.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationOptions.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NotificationService.cs | [x] | **FIXED 2026-01-01** - Removed embedded NotificationOptions, NotificationUrgency, NotificationCloseReason, NotificationContext, NotificationActionEventArgs, NotificationClosedEventArgs, NotificationAction |
| NotificationUrgency.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NullAccessibilityService.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| NullInputMethodService.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| PortalFilePickerService.cs | [x] | **FIXED 2026-01-01** - Removed embedded FolderResult, FolderPickerResult, FolderPickerOptions, PortalFolderPickerService |
| PortalFolderPickerService.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| PreEditAttribute.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| PreEditAttributeType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| PreEditChangedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| PreferencesService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, JSON file storage with XDG |
| ScaleChangedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SecureStorageService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, secret-tool with AES fallback |
| ShareService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, xdg-open with portal fallback |
| SystemColors.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SystemTheme.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SystemThemeService.cs | [x] | **FIXED 2026-01-01** - Removed embedded SystemTheme, DesktopEnvironment, ThemeChangedEventArgs, SystemColors |
| SystemTrayService.cs | [x] | **FIXED 2026-01-01** - Removed embedded TrayMenuItem |
| TextCommittedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| TextRun.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| ThemeChangedEventArgs.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| TrayMenuItem.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| VersionTrackingService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, JSON tracking file |
| VideoAccelerationApi.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| VideoFrame.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| VideoProfile.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| VirtualizationExtensions.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| VirtualizationManager.cs | [x] | **FIXED 2026-01-01** - Removed embedded VirtualizationExtensions |
| WaylandDisplayWindow.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| X11DisplayWindow.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| X11InputMethodService.cs | [x] | **VERIFIED 2026-01-01** - Logic matches, X11 XIM interface |
---
## HOSTING
| File | Status | Notes |
|------|--------|-------|
| GtkMauiContext.cs | [x] | **ADDED 2026-01-01** - Was missing, created from decompiled |
| HandlerMappingExtensions.cs | [x] | **ADDED 2026-01-01** - Was missing, created from decompiled |
| LinuxAnimationManager.cs | [x] | **ADDED 2026-01-01** - Was missing, created from decompiled |
| LinuxMauiAppBuilderExtensions.cs | [x] | **FIXED 2026-01-01** - Added IDispatcherProvider, IDeviceInfo, IDeviceDisplay, IAppInfo, IConnectivity, GtkHostService registrations; fixed WebView to use GtkWebViewHandler |
| LinuxMauiContext.cs | [x] | **FIXED 2026-01-01** - Removed embedded classes (now separate files), added LinuxDispatcher using |
| LinuxProgramHost.cs | [x] | **FIXED 2026-01-01** - Added GtkHostService.Initialize call for WebView support |
| LinuxTicker.cs | [x] | **ADDED 2026-01-01** - Was missing, created from decompiled |
| LinuxViewRenderer.cs | [x] | **FIXED 2026-01-01** - Added ApplyShellColors(), FlyoutHeader rendering, FlyoutFooterText, MauiShellContent, ContentRenderer/ColorRefresher delegates, page BackgroundColor handling |
| MauiAppBuilderExtensions.cs | [x] | **DELETED 2026-01-01** - Not in decompiled, was outdated duplicate with wrong namespace |
| MauiHandlerExtensions.cs | [x] | **FIXED 2026-01-01** - Fixed WebView to use GtkWebViewHandler, FlexLayout to use LayoutHandler |
| ScopedLinuxMauiContext.cs | [x] | **ADDED 2026-01-01** - Was missing, created from decompiled |
---
## DISPATCHING
| File | Status | Notes |
|------|--------|-------|
| LinuxDispatcher.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, clean syntax |
| LinuxDispatcherProvider.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| LinuxDispatcherTimer.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, clean syntax |
---
## NATIVE
| File | Status | Notes |
|------|--------|-------|
| CairoNative.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| GdkNative.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| GLibNative.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| GtkNative.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| WebKitNative.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
---
## WINDOW
| File | Status | Notes |
|------|--------|-------|
| CursorType.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| GtkHostWindow.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, main has clean comments |
| WaylandWindow.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, main has clean comments |
| X11Window.cs | [x] | **FIXED 2026-01-01** - Added SVG icon support, event counter logging from decompiled |
---
## RENDERING
| File | Status | Notes |
|------|--------|-------|
| GpuRenderingEngine.cs | [x] | **FIXED 2026-01-01** - Removed embedded GpuStats |
| GpuStats.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| GtkSkiaSurfaceWidget.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, same public API |
| LayeredRenderer.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| RenderCache.cs | [x] | **FIXED 2026-01-01** - Removed embedded LayeredRenderer, RenderLayer, TextRenderCache |
| RenderLayer.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| ResourceCache.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
| SkiaRenderingEngine.cs | [x] | **FIXED 2026-01-01** - Removed embedded ResourceCache |
| TextRenderCache.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled |
---
## INTEROP
| File | Status | Notes |
|------|--------|-------|
| ClientMessageData.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| WebKitGtk.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, main has cleaner formatting with regions |
| X11.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled, X11Interop.cs DELETED (was duplicate) |
| XButtonEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XClientMessageEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XConfigureEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XCrossingEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XCursorShape.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XEventMask.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XEventType.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XExposeEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XFocusChangeEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XKeyEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XMotionEvent.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XSetWindowAttributes.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| XWindowClass.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
---
## CONVERTERS
| File | Status | Notes |
|------|--------|-------|
| ColorExtensions.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SKColorTypeConverter.cs | [x] | **FIXED 2026-01-01** - Removed embedded ColorExtensions |
| SKPointTypeConverter.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SKRectTypeConverter.cs | [x] | **FIXED 2026-01-01** - Removed embedded SKSizeTypeConverter, SKPointTypeConverter, SKTypeExtensions |
| SKSizeTypeConverter.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
| SKTypeExtensions.cs | [x] | **VERIFIED 2026-01-01** - Separate file, matches decompiled |
---
## CORE
| File | Status | Notes |
|------|--------|-------|
| LinuxApplication.cs | [x] | **FIXED 2026-01-01** - Removed embedded DisplayServerType, LinuxApplicationOptions |
| LinuxApplicationOptions.cs | [x] | **VERIFIED 2026-01-01** - Matches decompiled (separate file) |
---
## TYPES
| File | Status | Notes |
|------|--------|-------|
| ToggledEventArgs.cs | [x] | ADDED - was missing, required by SkiaSwitch |

80
Native/CairoNative.cs Normal file
View File

@@ -0,0 +1,80 @@
using System;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Native;
internal static class CairoNative
{
public enum cairo_format_t
{
CAIRO_FORMAT_INVALID = -1,
CAIRO_FORMAT_ARGB32,
CAIRO_FORMAT_RGB24,
CAIRO_FORMAT_A8,
CAIRO_FORMAT_A1,
CAIRO_FORMAT_RGB16_565,
CAIRO_FORMAT_RGB30
}
private const string Lib = "libcairo.so.2";
[DllImport("libcairo.so.2")]
public static extern IntPtr cairo_image_surface_create_for_data(IntPtr data, cairo_format_t format, int width, int height, int stride);
[DllImport("libcairo.so.2")]
public static extern IntPtr cairo_image_surface_create(cairo_format_t format, int width, int height);
[DllImport("libcairo.so.2")]
public static extern IntPtr cairo_image_surface_get_data(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern int cairo_image_surface_get_width(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern int cairo_image_surface_get_height(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern int cairo_image_surface_get_stride(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern void cairo_surface_destroy(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern void cairo_surface_flush(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern void cairo_surface_mark_dirty(IntPtr surface);
[DllImport("libcairo.so.2")]
public static extern void cairo_surface_mark_dirty_rectangle(IntPtr surface, int x, int y, int width, int height);
[DllImport("libcairo.so.2")]
public static extern void cairo_set_source_surface(IntPtr cr, IntPtr surface, double x, double y);
[DllImport("libcairo.so.2")]
public static extern void cairo_set_source_rgb(IntPtr cr, double red, double green, double blue);
[DllImport("libcairo.so.2")]
public static extern void cairo_set_source_rgba(IntPtr cr, double red, double green, double blue, double alpha);
[DllImport("libcairo.so.2")]
public static extern void cairo_paint(IntPtr cr);
[DllImport("libcairo.so.2")]
public static extern void cairo_paint_with_alpha(IntPtr cr, double alpha);
[DllImport("libcairo.so.2")]
public static extern void cairo_fill(IntPtr cr);
[DllImport("libcairo.so.2")]
public static extern void cairo_rectangle(IntPtr cr, double x, double y, double width, double height);
[DllImport("libcairo.so.2")]
public static extern void cairo_clip(IntPtr cr);
[DllImport("libcairo.so.2")]
public static extern void cairo_save(IntPtr cr);
[DllImport("libcairo.so.2")]
public static extern void cairo_restore(IntPtr cr);
}

111
Native/GLibNative.cs Normal file
View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Native;
public static class GLibNative
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate bool GSourceFunc(IntPtr userData);
private const string Lib = "libglib-2.0.so.0";
private static readonly List<GSourceFunc> _callbacks = new List<GSourceFunc>();
private static readonly object _callbackLock = new object();
[DllImport("libglib-2.0.so.0", EntryPoint = "g_idle_add")]
private static extern uint g_idle_add_native(GSourceFunc function, IntPtr data);
[DllImport("libglib-2.0.so.0", EntryPoint = "g_timeout_add")]
private static extern uint g_timeout_add_native(uint interval, GSourceFunc function, IntPtr data);
[DllImport("libglib-2.0.so.0", EntryPoint = "g_source_remove")]
public static extern bool SourceRemove(uint sourceId);
[DllImport("libglib-2.0.so.0", EntryPoint = "g_get_monotonic_time")]
public static extern long GetMonotonicTime();
public static uint IdleAdd(Func<bool> callback)
{
GSourceFunc wrapper = null!;
wrapper = delegate
{
bool flag = false;
try
{
flag = callback();
}
catch (Exception ex)
{
Console.WriteLine("[GLibNative] Error in idle callback: " + ex.Message);
}
if (!flag)
{
lock (_callbackLock)
{
_callbacks.Remove(wrapper);
}
}
return flag;
};
lock (_callbackLock)
{
_callbacks.Add(wrapper);
}
return g_idle_add_native(wrapper, IntPtr.Zero);
}
public static uint TimeoutAdd(uint intervalMs, Func<bool> callback)
{
GSourceFunc wrapper = null!;
wrapper = delegate
{
bool flag = false;
try
{
flag = callback();
}
catch (Exception ex)
{
Console.WriteLine("[GLibNative] Error in timeout callback: " + ex.Message);
}
if (!flag)
{
lock (_callbackLock)
{
_callbacks.Remove(wrapper);
}
}
return flag;
};
lock (_callbackLock)
{
_callbacks.Add(wrapper);
}
return g_timeout_add_native(intervalMs, wrapper, IntPtr.Zero);
}
public static void ClearCallbacks()
{
lock (_callbackLock)
{
_callbacks.Clear();
}
}
public static uint g_idle_add(GSourceFunc func, IntPtr data)
{
return g_idle_add_native(func, data);
}
public static uint g_timeout_add(uint intervalMs, GSourceFunc func, IntPtr data)
{
return g_timeout_add_native(intervalMs, func, data);
}
public static bool g_source_remove(uint tag)
{
return SourceRemove(tag);
}
}

132
Native/GdkNative.cs Normal file
View File

@@ -0,0 +1,132 @@
using System;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Native;
internal static class GdkNative
{
[Flags]
public enum GdkEventMask
{
ExposureMask = 2,
PointerMotionMask = 4,
PointerMotionHintMask = 8,
ButtonMotionMask = 0x10,
Button1MotionMask = 0x20,
Button2MotionMask = 0x40,
Button3MotionMask = 0x80,
ButtonPressMask = 0x100,
ButtonReleaseMask = 0x200,
KeyPressMask = 0x400,
KeyReleaseMask = 0x800,
EnterNotifyMask = 0x1000,
LeaveNotifyMask = 0x2000,
FocusChangeMask = 0x4000,
StructureMask = 0x8000,
PropertyChangeMask = 0x10000,
VisibilityNotifyMask = 0x20000,
ProximityInMask = 0x40000,
ProximityOutMask = 0x80000,
SubstructureMask = 0x100000,
ScrollMask = 0x200000,
TouchMask = 0x400000,
SmoothScrollMask = 0x800000,
AllEventsMask = 0xFFFFFE
}
public enum GdkScrollDirection
{
Up,
Down,
Left,
Right,
Smooth
}
public struct GdkEventButton
{
public int Type;
public IntPtr Window;
public sbyte SendEvent;
public uint Time;
public double X;
public double Y;
public IntPtr Axes;
public uint State;
public uint Button;
public IntPtr Device;
public double XRoot;
public double YRoot;
}
public struct GdkEventMotion
{
public int Type;
public IntPtr Window;
public sbyte SendEvent;
public uint Time;
public double X;
public double Y;
public IntPtr Axes;
public uint State;
public short IsHint;
public IntPtr Device;
public double XRoot;
public double YRoot;
}
public struct GdkEventKey
{
public int Type;
public IntPtr Window;
public sbyte SendEvent;
public uint Time;
public uint State;
public uint Keyval;
public int Length;
public IntPtr String;
public ushort HardwareKeycode;
public byte Group;
public uint IsModifier;
}
public struct GdkEventScroll
{
public int Type;
public IntPtr Window;
public sbyte SendEvent;
public uint Time;
public double X;
public double Y;
public uint State;
public GdkScrollDirection Direction;
public IntPtr Device;
public double XRoot;
public double YRoot;
public double DeltaX;
public double DeltaY;
}
private const string Lib = "libgdk-3.so.0";
[DllImport("libgdk-3.so.0")]
public static extern IntPtr gdk_display_get_default();
[DllImport("libgdk-3.so.0")]
public static extern IntPtr gdk_display_get_name(IntPtr display);
[DllImport("libgdk-3.so.0")]
public static extern IntPtr gdk_screen_get_default();
[DllImport("libgdk-3.so.0")]
public static extern int gdk_screen_get_width(IntPtr screen);
[DllImport("libgdk-3.so.0")]
public static extern int gdk_screen_get_height(IntPtr screen);
[DllImport("libgdk-3.so.0")]
public static extern void gdk_window_invalidate_rect(IntPtr window, IntPtr rect, bool invalidateChildren);
[DllImport("libgdk-3.so.0")]
public static extern uint gdk_keyval_to_unicode(uint keyval);
}

192
Native/GtkNative.cs Normal file
View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Native;
internal static class GtkNative
{
public struct GtkAllocation
{
public int X;
public int Y;
public int Width;
public int Height;
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate bool GSourceFunc(IntPtr userData);
private const string Lib = "libgtk-3.so.0";
public const int GTK_WINDOW_TOPLEVEL = 0;
public const int GTK_WINDOW_POPUP = 1;
private const string LibGdkPixbuf = "libgdk_pixbuf-2.0.so.0";
public const int GDK_COLORSPACE_RGB = 0;
private const string GLib = "libglib-2.0.so.0";
private static readonly List<GSourceFunc> _idleCallbacks = new List<GSourceFunc>();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_init(ref int argc, ref IntPtr argv);
[DllImport("libgtk-3.so.0")]
public static extern bool gtk_init_check(ref int argc, ref IntPtr argv);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_window_new(int windowType);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_set_title(IntPtr window, string title);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_set_default_size(IntPtr window, int width, int height);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_resize(IntPtr window, int width, int height);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_move(IntPtr window, int x, int y);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_get_size(IntPtr window, out int width, out int height);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_get_position(IntPtr window, out int x, out int y);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_set_icon(IntPtr window, IntPtr pixbuf);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_window_set_icon_from_file(IntPtr window, string filename, IntPtr error);
[DllImport("libgdk_pixbuf-2.0.so.0")]
public static extern IntPtr gdk_pixbuf_new_from_file(string filename, IntPtr error);
[DllImport("libgdk_pixbuf-2.0.so.0")]
public static extern IntPtr gdk_pixbuf_new_from_data(IntPtr data, int colorspace, bool hasAlpha, int bitsPerSample, int width, int height, int rowstride, IntPtr destroyFn, IntPtr destroyFnData);
[DllImport("libgdk_pixbuf-2.0.so.0")]
public static extern void g_object_unref(IntPtr obj);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_show_all(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_show(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_hide(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_destroy(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_queue_draw(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_set_size_request(IntPtr widget, int width, int height);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_get_allocation(IntPtr widget, out GtkAllocation allocation);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_main();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_main_quit();
[DllImport("libgtk-3.so.0")]
public static extern bool gtk_events_pending();
[DllImport("libgtk-3.so.0")]
public static extern bool gtk_main_iteration_do(bool blocking);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_overlay_new();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_container_add(IntPtr container, IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_container_remove(IntPtr container, IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_overlay_add_overlay(IntPtr overlay, IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_overlay_set_overlay_pass_through(IntPtr overlay, IntPtr widget, bool passThrough);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_fixed_new();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_fixed_put(IntPtr fixedWidget, IntPtr widget, int x, int y);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_fixed_move(IntPtr fixedWidget, IntPtr widget, int x, int y);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_drawing_area_new();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_set_can_focus(IntPtr widget, bool canFocus);
[DllImport("libgtk-3.so.0")]
public static extern bool gtk_widget_grab_focus(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern bool gtk_widget_has_focus(IntPtr widget);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_add_events(IntPtr widget, int events);
[DllImport("libgtk-3.so.0")]
public static extern ulong g_signal_connect_data(IntPtr instance, string detailedSignal, IntPtr cHandler, IntPtr data, IntPtr destroyData, int connectFlags);
[DllImport("libgtk-3.so.0")]
public static extern void g_signal_handler_disconnect(IntPtr instance, ulong handlerId);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_widget_get_window(IntPtr widget);
[DllImport("libglib-2.0.so.0", EntryPoint = "g_idle_add")]
public static extern uint IdleAdd(GSourceFunc function, IntPtr data);
[DllImport("libglib-2.0.so.0", EntryPoint = "g_source_remove")]
public static extern bool SourceRemove(uint sourceId);
public static uint IdleAdd(Func<bool> callback)
{
GSourceFunc gSourceFunc = (IntPtr _) => callback();
_idleCallbacks.Add(gSourceFunc);
return IdleAdd(gSourceFunc, IntPtr.Zero);
}
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_menu_new();
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_menu_item_new_with_label(string label);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_separator_menu_item_new();
[DllImport("libgtk-3.so.0")]
public static extern void gtk_menu_shell_append(IntPtr menuShell, IntPtr child);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_menu_popup_at_pointer(IntPtr menu, IntPtr triggerEvent);
[DllImport("libgtk-3.so.0")]
public static extern void gtk_widget_set_sensitive(IntPtr widget, bool sensitive);
[DllImport("libgtk-3.so.0")]
public static extern IntPtr gtk_get_current_event();
[DllImport("libgdk-3.so.0")]
public static extern void gdk_event_free(IntPtr eventPtr);
}

256
Native/WebKitNative.cs Normal file
View File

@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Native;
internal static class WebKitNative
{
private delegate IntPtr WebKitWebViewNewDelegate();
private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, string uri);
private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, string content, string? baseUri);
private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView);
private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView);
private delegate void WebKitWebViewGoBackDelegate(IntPtr webView);
private delegate void WebKitWebViewGoForwardDelegate(IntPtr webView);
private delegate bool WebKitWebViewCanGoBackDelegate(IntPtr webView);
private delegate bool WebKitWebViewCanGoForwardDelegate(IntPtr webView);
private delegate void WebKitWebViewReloadDelegate(IntPtr webView);
private delegate void WebKitWebViewStopLoadingDelegate(IntPtr webView);
private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView);
private delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy);
private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData);
private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, LoadChangedCallback callback, IntPtr userData, IntPtr destroyNotify, int connectFlags);
public enum WebKitLoadEvent
{
Started,
Redirected,
Committed,
Finished
}
private static IntPtr _handle;
private static bool _initialized;
private static readonly string[] LibraryNames = new string[4]
{
"libwebkit2gtk-4.1.so.0",
"libwebkit2gtk-4.0.so.37",
"libwebkit2gtk-4.0.so",
"libwebkit2gtk-4.1.so"
};
private static WebKitWebViewNewDelegate? _webkitWebViewNew;
private static WebKitWebViewLoadUriDelegate? _webkitLoadUri;
private static WebKitWebViewLoadHtmlDelegate? _webkitLoadHtml;
private static WebKitWebViewGetUriDelegate? _webkitGetUri;
private static WebKitWebViewGetTitleDelegate? _webkitGetTitle;
private static WebKitWebViewGoBackDelegate? _webkitGoBack;
private static WebKitWebViewGoForwardDelegate? _webkitGoForward;
private static WebKitWebViewCanGoBackDelegate? _webkitCanGoBack;
private static WebKitWebViewCanGoForwardDelegate? _webkitCanGoForward;
private static WebKitWebViewReloadDelegate? _webkitReload;
private static WebKitWebViewStopLoadingDelegate? _webkitStopLoading;
private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings;
private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAccel;
private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript;
private static GSignalConnectDataDelegate? _gSignalConnectData;
private static readonly Dictionary<IntPtr, LoadChangedCallback> _loadChangedCallbacks = new Dictionary<IntPtr, LoadChangedCallback>();
private const int RTLD_NOW = 2;
private const int RTLD_GLOBAL = 256;
private static IntPtr _gobjectHandle;
[DllImport("libdl.so.2")]
private static extern IntPtr dlopen(string? filename, int flags);
[DllImport("libdl.so.2")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so.2")]
private static extern IntPtr dlerror();
public static bool Initialize()
{
if (_initialized)
{
return _handle != IntPtr.Zero;
}
_initialized = true;
string[] libraryNames = LibraryNames;
foreach (string text in libraryNames)
{
_handle = dlopen(text, 258);
if (_handle != IntPtr.Zero)
{
Console.WriteLine("[WebKitNative] Loaded " + text);
break;
}
}
if (_handle == IntPtr.Zero)
{
Console.WriteLine("[WebKitNative] Failed to load WebKitGTK library");
return false;
}
_webkitWebViewNew = LoadFunction<WebKitWebViewNewDelegate>("webkit_web_view_new");
_webkitLoadUri = LoadFunction<WebKitWebViewLoadUriDelegate>("webkit_web_view_load_uri");
_webkitLoadHtml = LoadFunction<WebKitWebViewLoadHtmlDelegate>("webkit_web_view_load_html");
_webkitGetUri = LoadFunction<WebKitWebViewGetUriDelegate>("webkit_web_view_get_uri");
_webkitGetTitle = LoadFunction<WebKitWebViewGetTitleDelegate>("webkit_web_view_get_title");
_webkitGoBack = LoadFunction<WebKitWebViewGoBackDelegate>("webkit_web_view_go_back");
_webkitGoForward = LoadFunction<WebKitWebViewGoForwardDelegate>("webkit_web_view_go_forward");
_webkitCanGoBack = LoadFunction<WebKitWebViewCanGoBackDelegate>("webkit_web_view_can_go_back");
_webkitCanGoForward = LoadFunction<WebKitWebViewCanGoForwardDelegate>("webkit_web_view_can_go_forward");
_webkitReload = LoadFunction<WebKitWebViewReloadDelegate>("webkit_web_view_reload");
_webkitStopLoading = LoadFunction<WebKitWebViewStopLoadingDelegate>("webkit_web_view_stop_loading");
_webkitGetSettings = LoadFunction<WebKitWebViewGetSettingsDelegate>("webkit_web_view_get_settings");
_webkitSetHardwareAccel = LoadFunction<WebKitSettingsSetHardwareAccelerationPolicyDelegate>("webkit_settings_set_hardware_acceleration_policy");
_webkitSetJavascript = LoadFunction<WebKitSettingsSetEnableJavascriptDelegate>("webkit_settings_set_enable_javascript");
_gobjectHandle = dlopen("libgobject-2.0.so.0", 258);
if (_gobjectHandle != IntPtr.Zero)
{
IntPtr intPtr = dlsym(_gobjectHandle, "g_signal_connect_data");
if (intPtr != IntPtr.Zero)
{
_gSignalConnectData = Marshal.GetDelegateForFunctionPointer<GSignalConnectDataDelegate>(intPtr);
Console.WriteLine("[WebKitNative] Loaded g_signal_connect_data");
}
}
return _webkitWebViewNew != null;
}
private static T? LoadFunction<T>(string name) where T : Delegate
{
if (_handle == IntPtr.Zero)
{
return null;
}
IntPtr intPtr = dlsym(_handle, name);
if (intPtr == IntPtr.Zero)
{
return null;
}
return Marshal.GetDelegateForFunctionPointer<T>(intPtr);
}
public static IntPtr WebViewNew()
{
if (!Initialize() || _webkitWebViewNew == null)
{
return IntPtr.Zero;
}
return _webkitWebViewNew();
}
public static void LoadUri(IntPtr webView, string uri)
{
_webkitLoadUri?.Invoke(webView, uri);
}
public static void LoadHtml(IntPtr webView, string content, string? baseUri = null)
{
_webkitLoadHtml?.Invoke(webView, content, baseUri);
}
public static string? GetUri(IntPtr webView)
{
IntPtr intPtr = _webkitGetUri?.Invoke(webView) ?? IntPtr.Zero;
if (intPtr == IntPtr.Zero)
{
return null;
}
return Marshal.PtrToStringUTF8(intPtr);
}
public static string? GetTitle(IntPtr webView)
{
IntPtr intPtr = _webkitGetTitle?.Invoke(webView) ?? IntPtr.Zero;
if (intPtr == IntPtr.Zero)
{
return null;
}
return Marshal.PtrToStringUTF8(intPtr);
}
public static void GoBack(IntPtr webView)
{
_webkitGoBack?.Invoke(webView);
}
public static void GoForward(IntPtr webView)
{
_webkitGoForward?.Invoke(webView);
}
public static bool CanGoBack(IntPtr webView)
{
return _webkitCanGoBack?.Invoke(webView) ?? false;
}
public static bool CanGoForward(IntPtr webView)
{
return _webkitCanGoForward?.Invoke(webView) ?? false;
}
public static void Reload(IntPtr webView)
{
_webkitReload?.Invoke(webView);
}
public static void StopLoading(IntPtr webView)
{
_webkitStopLoading?.Invoke(webView);
}
public static void ConfigureSettings(IntPtr webView, bool disableHardwareAccel = true)
{
if (_webkitGetSettings != null)
{
IntPtr intPtr = _webkitGetSettings(webView);
if (intPtr != IntPtr.Zero && disableHardwareAccel && _webkitSetHardwareAccel != null)
{
_webkitSetHardwareAccel(intPtr, 2);
}
}
}
public static void SetJavascriptEnabled(IntPtr webView, bool enabled)
{
if (_webkitGetSettings != null && _webkitSetJavascript != null)
{
IntPtr intPtr = _webkitGetSettings(webView);
if (intPtr != IntPtr.Zero)
{
_webkitSetJavascript(intPtr, enabled);
}
}
}
public static ulong ConnectLoadChanged(IntPtr webView, LoadChangedCallback callback)
{
if (_gSignalConnectData == null || webView == IntPtr.Zero)
{
Console.WriteLine("[WebKitNative] Cannot connect load-changed: signal connect not available");
return 0uL;
}
_loadChangedCallbacks[webView] = callback;
return _gSignalConnectData(webView, "load-changed", callback, IntPtr.Zero, IntPtr.Zero, 0);
}
public static void DisconnectLoadChanged(IntPtr webView)
{
_loadChangedCallbacks.Remove(webView);
}
}

View File

@@ -42,6 +42,7 @@
<PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.88.9" />
<PackageReference Include="Svg.Skia" Version="1.0.0" />
<!-- HarfBuzz for advanced text shaping -->
<PackageReference Include="HarfBuzzSharp" Version="7.3.0.3" />

View File

@@ -333,17 +333,3 @@ public class GpuRenderingEngine : IDisposable
GC.SuppressFinalize(this);
}
}
/// <summary>
/// GPU performance statistics.
/// </summary>
public class GpuStats
{
public bool IsGpuAccelerated { get; init; }
public int MaxTextureSize { get; init; }
public long ResourceCacheUsedBytes { get; init; }
public long ResourceCacheLimitBytes { get; init; }
public double ResourceCacheUsedMB => ResourceCacheUsedBytes / (1024.0 * 1024.0);
public double ResourceCacheLimitMB => ResourceCacheLimitBytes / (1024.0 * 1024.0);
}

19
Rendering/GpuStats.cs Normal file
View File

@@ -0,0 +1,19 @@
// 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.Rendering;
public class GpuStats
{
public bool IsGpuAccelerated { get; init; }
public int MaxTextureSize { get; init; }
public long ResourceCacheUsedBytes { get; init; }
public long ResourceCacheLimitBytes { get; init; }
public double ResourceCacheUsedMB => ResourceCacheUsedBytes / 1048576.0;
public double ResourceCacheLimitMB => ResourceCacheLimitBytes / 1048576.0;
}

View File

@@ -0,0 +1,391 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Native;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
/// <summary>
/// GTK drawing area widget that renders Skia content via Cairo.
/// Provides hardware-accelerated 2D rendering for MAUI views.
/// </summary>
public sealed class GtkSkiaSurfaceWidget : IDisposable
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool DrawCallback(IntPtr widget, IntPtr cairoContext, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool ConfigureCallback(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool ButtonEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool MotionEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool KeyEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool ScrollEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData);
private struct GdkEventButton
{
public int type;
public IntPtr window;
public sbyte send_event;
public uint time;
public double x;
public double y;
public IntPtr axes;
public uint state;
public uint button;
}
private struct GdkEventMotion
{
public int type;
public IntPtr window;
public sbyte send_event;
public uint time;
public double x;
public double y;
}
private struct GdkEventKey
{
public int type;
public IntPtr window;
public sbyte send_event;
public uint time;
public uint state;
public uint keyval;
public int length;
public IntPtr str;
public ushort hardware_keycode;
}
private struct GdkEventScroll
{
public int type;
public IntPtr window;
public sbyte send_event;
public uint time;
public double x;
public double y;
public uint state;
public int direction;
public IntPtr device;
public double x_root;
public double y_root;
public double delta_x;
public double delta_y;
}
private IntPtr _widget;
private SKImageInfo _imageInfo;
private SKBitmap? _bitmap;
private SKCanvas? _canvas;
private IntPtr _cairoSurface;
private readonly DrawCallback _drawCallback;
private readonly ConfigureCallback _configureCallback;
private ulong _drawSignalId;
private ulong _configureSignalId;
private bool _isTransparent;
private readonly ButtonEventCallback _buttonPressCallback;
private readonly ButtonEventCallback _buttonReleaseCallback;
private readonly MotionEventCallback _motionCallback;
private readonly KeyEventCallback _keyPressCallback;
private readonly KeyEventCallback _keyReleaseCallback;
private readonly ScrollEventCallback _scrollCallback;
public IntPtr Widget => _widget;
public SKCanvas? Canvas => _canvas;
public SKImageInfo ImageInfo => _imageInfo;
public int Width => _imageInfo.Width;
public int Height => _imageInfo.Height;
public bool IsTransparent => _isTransparent;
public event EventHandler? DrawRequested;
public event EventHandler<(int Width, int Height)>? Resized;
public event EventHandler<(double X, double Y, int Button)>? PointerPressed;
public event EventHandler<(double X, double Y, int Button)>? PointerReleased;
public event EventHandler<(double X, double Y)>? PointerMoved;
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyPressed;
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyReleased;
public event EventHandler<(double X, double Y, double DeltaX, double DeltaY)>? Scrolled;
public event EventHandler<string>? TextInput;
public GtkSkiaSurfaceWidget(int width, int height)
{
_widget = GtkNative.gtk_drawing_area_new();
if (_widget == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create GTK drawing area");
}
GtkNative.gtk_widget_set_size_request(_widget, width, height);
GtkNative.gtk_widget_add_events(_widget, 10551046);
GtkNative.gtk_widget_set_can_focus(_widget, canFocus: true);
CreateBuffer(width, height);
// Store delegates to prevent garbage collection
_drawCallback = OnDraw;
_configureCallback = OnConfigure;
_buttonPressCallback = OnButtonPress;
_buttonReleaseCallback = OnButtonRelease;
_motionCallback = OnMotion;
_keyPressCallback = OnKeyPress;
_keyReleaseCallback = OnKeyRelease;
_scrollCallback = OnScroll;
// Connect signals
_drawSignalId = GtkNative.g_signal_connect_data(_widget, "draw", Marshal.GetFunctionPointerForDelegate(_drawCallback), IntPtr.Zero, IntPtr.Zero, 0);
_configureSignalId = GtkNative.g_signal_connect_data(_widget, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "key-press-event", Marshal.GetFunctionPointerForDelegate(_keyPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "key-release-event", Marshal.GetFunctionPointerForDelegate(_keyReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
GtkNative.g_signal_connect_data(_widget, "scroll-event", Marshal.GetFunctionPointerForDelegate(_scrollCallback), IntPtr.Zero, IntPtr.Zero, 0);
Console.WriteLine($"[GtkSkiaSurfaceWidget] Created with size {width}x{height}");
}
private void CreateBuffer(int width, int height)
{
width = Math.Max(1, width);
height = Math.Max(1, height);
_canvas?.Dispose();
_bitmap?.Dispose();
if (_cairoSurface != IntPtr.Zero)
{
CairoNative.cairo_surface_destroy(_cairoSurface);
_cairoSurface = IntPtr.Zero;
}
_imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
_bitmap = new SKBitmap(_imageInfo);
_canvas = new SKCanvas(_bitmap);
IntPtr pixels = _bitmap.GetPixels();
_cairoSurface = CairoNative.cairo_image_surface_create_for_data(
pixels,
CairoNative.cairo_format_t.CAIRO_FORMAT_ARGB32,
_imageInfo.Width,
_imageInfo.Height,
_imageInfo.RowBytes);
Console.WriteLine($"[GtkSkiaSurfaceWidget] Created buffer {width}x{height}, stride={_imageInfo.RowBytes}");
}
public void Resize(int width, int height)
{
if (width != _imageInfo.Width || height != _imageInfo.Height)
{
CreateBuffer(width, height);
Resized?.Invoke(this, (width, height));
}
}
public void RenderFrame(Action<SKCanvas, SKImageInfo> render)
{
if (_canvas != null && _bitmap != null)
{
render(_canvas, _imageInfo);
_canvas.Flush();
CairoNative.cairo_surface_flush(_cairoSurface);
CairoNative.cairo_surface_mark_dirty(_cairoSurface);
GtkNative.gtk_widget_queue_draw(_widget);
}
}
public void Invalidate()
{
GtkNative.gtk_widget_queue_draw(_widget);
}
public void SetTransparent(bool transparent)
{
_isTransparent = transparent;
}
private bool OnDraw(IntPtr widget, IntPtr cairoContext, IntPtr userData)
{
if (_cairoSurface == IntPtr.Zero || cairoContext == IntPtr.Zero)
{
return false;
}
if (_isTransparent)
{
_canvas?.Clear(SKColors.Transparent);
}
DrawRequested?.Invoke(this, EventArgs.Empty);
_canvas?.Flush();
CairoNative.cairo_surface_flush(_cairoSurface);
CairoNative.cairo_surface_mark_dirty(_cairoSurface);
CairoNative.cairo_set_source_surface(cairoContext, _cairoSurface, 0.0, 0.0);
CairoNative.cairo_paint(cairoContext);
return true;
}
private bool OnConfigure(IntPtr widget, IntPtr eventData, IntPtr userData)
{
GtkNative.gtk_widget_get_allocation(widget, out var allocation);
if (allocation.Width > 0 && allocation.Height > 0 &&
(allocation.Width != _imageInfo.Width || allocation.Height != _imageInfo.Height))
{
Resize(allocation.Width, allocation.Height);
}
return false;
}
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
{
GtkNative.gtk_widget_grab_focus(_widget);
var (x, y, button) = ParseButtonEvent(eventData);
Console.WriteLine($"[GtkSkiaSurfaceWidget] ButtonPress at ({x}, {y}), button={button}");
PointerPressed?.Invoke(this, (x, y, button));
return true;
}
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y, button) = ParseButtonEvent(eventData);
PointerReleased?.Invoke(this, (x, y, button));
return true;
}
private bool OnMotion(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y) = ParseMotionEvent(eventData);
PointerMoved?.Invoke(this, (x, y));
return true;
}
public void RaisePointerPressed(double x, double y, int button)
{
Console.WriteLine($"[GtkSkiaSurfaceWidget] RaisePointerPressed at ({x}, {y}), button={button}");
PointerPressed?.Invoke(this, (x, y, button));
}
public void RaisePointerReleased(double x, double y, int button)
{
PointerReleased?.Invoke(this, (x, y, button));
}
public void RaisePointerMoved(double x, double y)
{
PointerMoved?.Invoke(this, (x, y));
}
private bool OnKeyPress(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (keyval, keycode, state) = ParseKeyEvent(eventData);
KeyPressed?.Invoke(this, (keyval, keycode, state));
uint unicode = GdkNative.gdk_keyval_to_unicode(keyval);
if (unicode != 0 && unicode < 65536)
{
char c = (char)unicode;
if (!char.IsControl(c) || c == '\r' || c == '\n' || c == '\t')
{
string text = c.ToString();
Console.WriteLine($"[GtkSkiaSurfaceWidget] TextInput: '{text}' (keyval={keyval}, unicode={unicode})");
TextInput?.Invoke(this, text);
}
}
return true;
}
private bool OnKeyRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (keyval, keycode, state) = ParseKeyEvent(eventData);
KeyReleased?.Invoke(this, (keyval, keycode, state));
return true;
}
private bool OnScroll(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y, deltaX, deltaY) = ParseScrollEvent(eventData);
Scrolled?.Invoke(this, (x, y, deltaX, deltaY));
return true;
}
private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventButton>(eventData);
return (evt.x, evt.y, (int)evt.button);
}
private static (double x, double y) ParseMotionEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventMotion>(eventData);
return (evt.x, evt.y);
}
private static (uint keyval, uint keycode, uint state) ParseKeyEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventKey>(eventData);
return (evt.keyval, evt.hardware_keycode, evt.state);
}
private static (double x, double y, double deltaX, double deltaY) ParseScrollEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventScroll>(eventData);
double deltaX = 0.0;
double deltaY = 0.0;
if (evt.direction == 4) // GDK_SCROLL_SMOOTH
{
deltaX = evt.delta_x;
deltaY = evt.delta_y;
}
else
{
switch (evt.direction)
{
case 0: // GDK_SCROLL_UP
deltaY = -1.0;
break;
case 1: // GDK_SCROLL_DOWN
deltaY = 1.0;
break;
case 2: // GDK_SCROLL_LEFT
deltaX = -1.0;
break;
case 3: // GDK_SCROLL_RIGHT
deltaX = 1.0;
break;
}
}
return (evt.x, evt.y, deltaX, deltaY);
}
public void GrabFocus()
{
GtkNative.gtk_widget_grab_focus(_widget);
}
public void Dispose()
{
_canvas?.Dispose();
_canvas = null;
_bitmap?.Dispose();
_bitmap = null;
if (_cairoSurface != IntPtr.Zero)
{
CairoNative.cairo_surface_destroy(_cairoSurface);
_cairoSurface = IntPtr.Zero;
}
}
}

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 SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
public class LayeredRenderer : IDisposable
{
private readonly Dictionary<int, RenderLayer> _layers = new();
private readonly object _lock = new();
private bool _disposed;
public RenderLayer GetLayer(int zIndex)
{
lock (_lock)
{
if (!_layers.TryGetValue(zIndex, out var layer))
{
layer = new RenderLayer(zIndex);
_layers[zIndex] = layer;
}
return layer;
}
}
public void RemoveLayer(int zIndex)
{
lock (_lock)
{
if (_layers.TryGetValue(zIndex, out var layer))
{
layer.Dispose();
_layers.Remove(zIndex);
}
}
}
public void Composite(SKCanvas canvas, SKRect bounds)
{
lock (_lock)
{
foreach (var layer in _layers.Values.OrderBy(l => l.ZIndex))
{
layer.DrawTo(canvas, bounds);
}
}
}
public void InvalidateAll()
{
lock (_lock)
{
foreach (var layer in _layers.Values)
{
layer.Invalidate();
}
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
lock (_lock)
{
foreach (var layer in _layers.Values)
{
layer.Dispose();
}
_layers.Clear();
}
}
}

View File

@@ -236,291 +236,3 @@ public class RenderCache : IDisposable
public int AccessCount { get; set; }
}
}
/// <summary>
/// Provides layered rendering for separating static and dynamic content.
/// </summary>
public class LayeredRenderer : IDisposable
{
private readonly Dictionary<int, RenderLayer> _layers = new();
private readonly object _lock = new();
private bool _disposed;
/// <summary>
/// Gets or creates a render layer.
/// </summary>
public RenderLayer GetLayer(int zIndex)
{
lock (_lock)
{
if (!_layers.TryGetValue(zIndex, out var layer))
{
layer = new RenderLayer(zIndex);
_layers[zIndex] = layer;
}
return layer;
}
}
/// <summary>
/// Removes a render layer.
/// </summary>
public void RemoveLayer(int zIndex)
{
lock (_lock)
{
if (_layers.TryGetValue(zIndex, out var layer))
{
layer.Dispose();
_layers.Remove(zIndex);
}
}
}
/// <summary>
/// Composites all layers onto the target canvas.
/// </summary>
public void Composite(SKCanvas canvas, SKRect bounds)
{
lock (_lock)
{
foreach (var layer in _layers.Values.OrderBy(l => l.ZIndex))
{
layer.DrawTo(canvas, bounds);
}
}
}
/// <summary>
/// Invalidates all layers.
/// </summary>
public void InvalidateAll()
{
lock (_lock)
{
foreach (var layer in _layers.Values)
{
layer.Invalidate();
}
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
lock (_lock)
{
foreach (var layer in _layers.Values)
{
layer.Dispose();
}
_layers.Clear();
}
}
}
/// <summary>
/// Represents a single render layer with its own bitmap buffer.
/// </summary>
public class RenderLayer : IDisposable
{
private SKBitmap? _bitmap;
private SKCanvas? _canvas;
private bool _isDirty = true;
private SKRect _bounds;
private bool _disposed;
/// <summary>
/// Gets the Z-index of this layer.
/// </summary>
public int ZIndex { get; }
/// <summary>
/// Gets whether this layer needs to be redrawn.
/// </summary>
public bool IsDirty => _isDirty;
/// <summary>
/// Gets or sets whether this layer is visible.
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// Gets or sets the layer opacity (0-1).
/// </summary>
public float Opacity { get; set; } = 1f;
public RenderLayer(int zIndex)
{
ZIndex = zIndex;
}
/// <summary>
/// Prepares the layer for rendering.
/// </summary>
public SKCanvas BeginDraw(SKRect bounds)
{
if (_bitmap == null || _bounds != bounds)
{
_bitmap?.Dispose();
_canvas?.Dispose();
int width = Math.Max(1, (int)bounds.Width);
int height = Math.Max(1, (int)bounds.Height);
_bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
_canvas = new SKCanvas(_bitmap);
_bounds = bounds;
}
_canvas!.Clear(SKColors.Transparent);
_isDirty = false;
return _canvas;
}
/// <summary>
/// Marks the layer as needing redraw.
/// </summary>
public void Invalidate()
{
_isDirty = true;
}
/// <summary>
/// Draws this layer to the target canvas.
/// </summary>
public void DrawTo(SKCanvas canvas, SKRect bounds)
{
if (!IsVisible || _bitmap == null) return;
using var paint = new SKPaint
{
Color = SKColors.White.WithAlpha((byte)(Opacity * 255))
};
canvas.DrawBitmap(_bitmap, bounds.Left, bounds.Top, paint);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_canvas?.Dispose();
_bitmap?.Dispose();
}
}
/// <summary>
/// Provides text rendering optimization with glyph caching.
/// </summary>
public class TextRenderCache : IDisposable
{
private readonly Dictionary<TextCacheKey, SKBitmap> _cache = new();
private readonly object _lock = new();
private int _maxEntries = 500;
private bool _disposed;
/// <summary>
/// Gets or sets the maximum number of cached text entries.
/// </summary>
public int MaxEntries
{
get => _maxEntries;
set => _maxEntries = Math.Max(10, value);
}
/// <summary>
/// Gets a cached text bitmap or creates one.
/// </summary>
public SKBitmap GetOrCreate(string text, SKPaint paint)
{
var key = new TextCacheKey(text, paint);
lock (_lock)
{
if (_cache.TryGetValue(key, out var cached))
{
return cached;
}
// Create text bitmap
var bounds = new SKRect();
paint.MeasureText(text, ref bounds);
int width = Math.Max(1, (int)Math.Ceiling(bounds.Width) + 2);
int height = Math.Max(1, (int)Math.Ceiling(bounds.Height) + 2);
var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
canvas.DrawText(text, -bounds.Left + 1, -bounds.Top + 1, paint);
}
// Trim cache if needed
if (_cache.Count >= _maxEntries)
{
var oldest = _cache.First();
oldest.Value.Dispose();
_cache.Remove(oldest.Key);
}
_cache[key] = bitmap;
return bitmap;
}
}
/// <summary>
/// Clears all cached text.
/// </summary>
public void Clear()
{
lock (_lock)
{
foreach (var entry in _cache.Values)
{
entry.Dispose();
}
_cache.Clear();
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Clear();
}
private readonly struct TextCacheKey : IEquatable<TextCacheKey>
{
private readonly string _text;
private readonly float _textSize;
private readonly SKColor _color;
private readonly int _weight;
private readonly int _hashCode;
public TextCacheKey(string text, SKPaint paint)
{
_text = text;
_textSize = paint.TextSize;
_color = paint.Color;
_weight = paint.Typeface?.FontWeight ?? (int)SKFontStyleWeight.Normal;
_hashCode = HashCode.Combine(_text, _textSize, _color, _weight);
}
public bool Equals(TextCacheKey other)
{
return _text == other._text &&
Math.Abs(_textSize - other._textSize) < 0.001f &&
_color == other._color &&
_weight == other._weight;
}
public override bool Equals(object? obj) => obj is TextCacheKey other && Equals(other);
public override int GetHashCode() => _hashCode;
}
}

72
Rendering/RenderLayer.cs Normal file
View File

@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
public class RenderLayer : IDisposable
{
private SKBitmap? _bitmap;
private SKCanvas? _canvas;
private bool _isDirty = true;
private SKRect _bounds;
private bool _disposed;
public int ZIndex { get; }
public bool IsDirty => _isDirty;
public bool IsVisible { get; set; } = true;
public float Opacity { get; set; } = 1f;
public RenderLayer(int zIndex)
{
ZIndex = zIndex;
}
public SKCanvas BeginDraw(SKRect bounds)
{
if (_bitmap == null || _bounds != bounds)
{
_bitmap?.Dispose();
_canvas?.Dispose();
var width = Math.Max(1, (int)bounds.Width);
var height = Math.Max(1, (int)bounds.Height);
_bitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
_canvas = new SKCanvas(_bitmap);
_bounds = bounds;
}
_canvas!.Clear(SKColors.Transparent);
_isDirty = false;
return _canvas;
}
public void Invalidate()
{
_isDirty = true;
}
public void DrawTo(SKCanvas canvas, SKRect bounds)
{
if (!IsVisible || _bitmap == null) return;
using var paint = new SKPaint
{
Color = SKColors.White.WithAlpha((byte)(Opacity * 255f))
};
canvas.DrawBitmap(_bitmap, bounds.Left, bounds.Top, paint);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_canvas?.Dispose();
_bitmap?.Dispose();
}
}

View File

@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
public class ResourceCache : IDisposable
{
private readonly Dictionary<string, SKTypeface> _typefaces = new();
private bool _disposed;
public SKTypeface GetTypeface(string fontFamily, SKFontStyle style)
{
var key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}";
if (!_typefaces.TryGetValue(key, out var typeface))
{
typeface = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default;
_typefaces[key] = typeface;
}
return typeface;
}
public void Clear()
{
foreach (var typeface in _typefaces.Values)
{
typeface.Dispose();
}
_typefaces.Clear();
}
public void Dispose()
{
if (!_disposed)
{
Clear();
_disposed = true;
}
}
}

View File

@@ -313,31 +313,3 @@ public class SkiaRenderingEngine : IDisposable
GC.SuppressFinalize(this);
}
}
public class ResourceCache : IDisposable
{
private readonly Dictionary<string, SKTypeface> _typefaces = new();
private bool _disposed;
public SKTypeface GetTypeface(string fontFamily, SKFontStyle style)
{
var key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}";
if (!_typefaces.TryGetValue(key, out var typeface))
{
typeface = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default;
_typefaces[key] = typeface;
}
return typeface;
}
public void Clear()
{
foreach (var tf in _typefaces.Values) tf.Dispose();
_typefaces.Clear();
}
public void Dispose()
{
if (!_disposed) { Clear(); _disposed = true; }
}
}

View File

@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
public class TextRenderCache : IDisposable
{
private readonly struct TextCacheKey : IEquatable<TextCacheKey>
{
private readonly string _text;
private readonly float _textSize;
private readonly SKColor _color;
private readonly int _weight;
private readonly int _hashCode;
public TextCacheKey(string text, SKPaint paint)
{
_text = text;
_textSize = paint.TextSize;
_color = paint.Color;
_weight = paint.Typeface?.FontWeight ?? 400;
_hashCode = HashCode.Combine(_text, _textSize, _color, _weight);
}
public bool Equals(TextCacheKey other)
{
return _text == other._text
&& Math.Abs(_textSize - other._textSize) < 0.001f
&& _color == other._color
&& _weight == other._weight;
}
public override bool Equals(object? obj)
{
return obj is TextCacheKey other && Equals(other);
}
public override int GetHashCode() => _hashCode;
}
private readonly Dictionary<TextCacheKey, SKBitmap> _cache = new();
private readonly object _lock = new();
private int _maxEntries = 500;
private bool _disposed;
public int MaxEntries
{
get => _maxEntries;
set => _maxEntries = Math.Max(10, value);
}
public SKBitmap GetOrCreate(string text, SKPaint paint)
{
var key = new TextCacheKey(text, paint);
lock (_lock)
{
if (_cache.TryGetValue(key, out var cached))
{
return cached;
}
var bounds = new SKRect();
paint.MeasureText(text, ref bounds);
var width = Math.Max(1, (int)Math.Ceiling(bounds.Width) + 2);
var height = Math.Max(1, (int)Math.Ceiling(bounds.Height) + 2);
var bitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
canvas.DrawText(text, -bounds.Left + 1f, -bounds.Top + 1f, paint);
if (_cache.Count >= _maxEntries)
{
var first = _cache.First();
first.Value.Dispose();
_cache.Remove(first.Key);
}
_cache[key] = bitmap;
return bitmap;
}
}
public void Clear()
{
lock (_lock)
{
foreach (var bitmap in _cache.Values)
{
bitmap.Dispose();
}
_cache.Clear();
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
Clear();
}
}
}

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