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>
This commit is contained in:
2026-01-01 11:19:58 -05:00
parent e02af03be0
commit f7043ab9c7
56 changed files with 6061 additions and 473 deletions

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

View File

@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -68,7 +69,7 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
if (content.Handler == null) if (content.Handler == null)
{ {
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}"); 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) if (content.Handler?.PlatformView is SkiaView skiaContent)

View File

@@ -18,6 +18,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
[nameof(ICheckBox.IsChecked)] = MapIsChecked, [nameof(ICheckBox.IsChecked)] = MapIsChecked,
[nameof(ICheckBox.Foreground)] = MapForeground, [nameof(ICheckBox.Foreground)] = MapForeground,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, [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) public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;

View File

@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -158,7 +159,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
// Create handler for the view // Create handler for the view
if (view.Handler == null && handler.MauiContext != null) 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) if (view.Handler?.PlatformView is SkiaView skiaView)
@@ -174,7 +175,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
{ {
if (cellView.Handler == null && handler.MauiContext != null) 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) if (cellView.Handler?.PlatformView is SkiaView skiaView)

View File

@@ -49,6 +49,17 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
{ {
base.ConnectHandler(platformView); base.ConnectHandler(platformView);
platformView.DateSelected += OnDateSelected; 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) protected override void DisconnectHandler(SkiaDatePicker platformView)

View File

@@ -3,6 +3,7 @@
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -31,6 +32,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
}; };
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) 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(); 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.Controls;
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -92,7 +93,7 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
// Create handler for content if it doesn't exist // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
content.Handler = content.ToHandler(handler.MauiContext); content.Handler = content.ToViewHandler(handler.MauiContext);
} }
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)

439
Handlers/GestureManager.cs Normal file
View File

@@ -0,0 +1,439 @@
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();
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new();
private const double SwipeMinDistance = 50.0;
private const double SwipeMaxTime = 500.0;
private const double SwipeDirectionThreshold = 0.5;
private const double PanMinDistance = 10.0;
/// <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?.Count > 0 && ProcessTapOnView(current, x, y))
{
return true;
}
current = current.Parent as View;
}
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 recognizer in recognizers)
{
if (recognizer is not TapGestureRecognizer tapRecognizer)
continue;
Console.WriteLine($"[GestureManager] Processing TapGestureRecognizer on {view.GetType().Name}, CommandParameter={tapRecognizer.CommandParameter}, NumberOfTapsRequired={tapRecognizer.NumberOfTapsRequired}");
int requiredTaps = tapRecognizer.NumberOfTapsRequired;
if (requiredTaps > 1)
{
var now = DateTime.UtcNow;
if (!_tapTracking.TryGetValue(view, out var tracking))
{
_tapTracking[view] = (now, 1);
Console.WriteLine($"[GestureManager] First tap 1/{requiredTaps}");
continue;
}
if ((now - tracking.lastTap).TotalMilliseconds >= 300.0)
{
_tapTracking[view] = (now, 1);
Console.WriteLine($"[GestureManager] Tap timeout, reset to 1/{requiredTaps}");
continue;
}
int tapCount = tracking.tapCount + 1;
if (tapCount < requiredTaps)
{
_tapTracking[view] = (now, tapCount);
Console.WriteLine($"[GestureManager] Tap {tapCount}/{requiredTaps}, waiting for more taps");
continue;
}
_tapTracking.Remove(view);
}
bool eventFired = false;
// Try SendTapped method
try
{
_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);
}
// Try direct event invocation
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?.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);
}
}
// Execute command if available
if (tapRecognizer.Command?.CanExecute(tapRecognizer.CommandParameter) == true)
{
Console.WriteLine("[GestureManager] Executing Command");
tapRecognizer.Command.Execute(tapRecognizer.CommandParameter);
}
result = true;
}
return result;
}
/// <summary>
/// Checks if the view has any gesture recognizers.
/// </summary>
public static bool HasGestureRecognizers(View? view)
{
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;
return view.GestureRecognizers.Any(g => g is TapGestureRecognizer);
}
/// <summary>
/// Processes a pointer down event.
/// </summary>
public static void ProcessPointerDown(View? view, double x, double y)
{
if (view == null) return;
_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;
double distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance >= PanMinDistance)
{
ProcessPanGesture(view, deltaX, deltaY, state.IsPanning ? GestureStatus.Running : GestureStatus.Started);
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;
// Check for swipe
if (distance >= SwipeMinDistance && elapsed <= SwipeMaxTime)
{
var direction = DetermineSwipeDirection(deltaX, deltaY);
ProcessSwipeGesture(view, direction);
}
// Complete pan or detect tap
if (state.IsPanning)
{
ProcessPanGesture(view, deltaX, deltaY, GestureStatus.Completed);
}
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 * SwipeDirectionThreshold)
return deltaX > 0 ? SwipeDirection.Right : SwipeDirection.Left;
if (absY > absX * SwipeDirectionThreshold)
return deltaY > 0 ? SwipeDirection.Down : SwipeDirection.Up;
return deltaX > 0 ? SwipeDirection.Right : SwipeDirection.Left;
}
private static void ProcessSwipeGesture(View view, SwipeDirection direction)
{
var recognizers = view.GestureRecognizers;
if (recognizers == null) return;
foreach (var recognizer in recognizers)
{
if (recognizer is not SwipeGestureRecognizer swipeRecognizer)
continue;
if (!swipeRecognizer.Direction.HasFlag(direction))
continue;
Console.WriteLine($"[GestureManager] Swipe detected: {direction}");
try
{
var method = typeof(SwipeGestureRecognizer).GetMethod("SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
method?.Invoke(swipeRecognizer, new object[] { view, direction });
}
catch (Exception ex)
{
Console.WriteLine("[GestureManager] SendSwiped failed: " + ex.Message);
}
if (swipeRecognizer.Command?.CanExecute(swipeRecognizer.CommandParameter) == true)
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 recognizer in recognizers)
{
if (recognizer is not PanGestureRecognizer panRecognizer)
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);
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 recognizer in recognizers)
{
if (recognizer is not PointerGestureRecognizer pointerRecognizer)
continue;
try
{
string? methodName = eventType switch
{
PointerEventType.Entered => "SendPointerEntered",
PointerEventType.Exited => "SendPointerExited",
PointerEventType.Pressed => "SendPointerPressed",
PointerEventType.Moved => "SendPointerMoved",
PointerEventType.Released => "SendPointerReleased",
_ => null
};
if (methodName != null)
{
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(Array.Empty<object>());
}
}
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;
return view.GestureRecognizers.Any(g => g is SwipeGestureRecognizer);
}
/// <summary>
/// Checks if the view has a pan gesture recognizer.
/// </summary>
public static bool HasPanGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null) return false;
return view.GestureRecognizers.Any(g => g is PanGestureRecognizer);
}
/// <summary>
/// Checks if the view has a pointer gesture recognizer.
/// </summary>
public static bool HasPointerGestureRecognizer(View? view)
{
if (view?.GestureRecognizers == null) return false;
return view.GestureRecognizers.Any(g => g is PointerGestureRecognizer);
}
}

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

@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using SkiaSharp; using SkiaSharp;
@@ -20,6 +22,8 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
[nameof(IImage.IsOpaque)] = MapIsOpaque, [nameof(IImage.IsOpaque)] = MapIsOpaque,
[nameof(IImageSourcePart.Source)] = MapSource, [nameof(IImageSourcePart.Source)] = MapSource,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["Width"] = MapWidth,
["Height"] = MapHeight,
}; };
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) 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; 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(); 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 // Image source loading helper
private ImageSourceServiceResultManager _sourceLoader = null!; private ImageSourceServiceResultManager _sourceLoader = null!;
@@ -162,6 +209,14 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
await _handler.PlatformView!.LoadFromStreamAsync(stream); 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) 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,8 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Window;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -29,6 +32,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
["FormattedText"] = MapFormattedText,
}; };
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -49,6 +53,39 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
return new 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) public static void MapText(LabelHandler handler, ILabel label)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
@@ -205,4 +242,53 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
_ => LayoutOptions.Start _ => 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. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -78,7 +79,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
// Create handler for child if it doesn't exist // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToHandler(MauiContext); child.Handler = child.ToViewHandler(MauiContext);
} }
if (child.Handler?.PlatformView is SkiaView skiaChild) 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 // Create handler for child if it doesn't exist
if (child.Handler == null) 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"}"); 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.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; 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 // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToHandler(MauiContext); child.Handler = child.ToViewHandler(MauiContext);
} }
// Add child's platform view to our layout // 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 // Create handler for child if it doesn't exist
if (child.Handler == null) if (child.Handler == null)
{ {
child.Handler = child.ToHandler(MauiContext); child.Handler = child.ToViewHandler(MauiContext);
} }
// Get grid position from attached properties // Get grid position from attached properties

View File

@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
using System.Collections.Specialized; using System.Collections.Specialized;
@@ -100,7 +101,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (page.Handler == null) if (page.Handler == null)
{ {
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}"); 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}"); Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
@@ -122,7 +123,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}"); Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
if (contentPage.Content.Handler == null) 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) if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
{ {
@@ -221,7 +222,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
if (e.Page.Handler == null) if (e.Page.Handler == null)
{ {
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}"); 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}"); Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
} }
@@ -334,7 +335,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
// Ensure handler exists // Ensure handler exists
if (page.Handler == null) if (page.Handler == null)
{ {
page.Handler = page.ToHandler(handler.MauiContext); page.Handler = page.ToViewHandler(handler.MauiContext);
} }
if (page.Handler?.PlatformView is SkiaPage skiaPage) if (page.Handler?.PlatformView is SkiaPage skiaPage)

View File

@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Hosting;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers; namespace Microsoft.Maui.Platform.Linux.Handlers;
@@ -144,7 +145,7 @@ public partial class ContentPageHandler : PageHandler
if (content.Handler == null) if (content.Handler == null)
{ {
Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}"); 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 // The content's handler should provide the platform view

View File

@@ -22,6 +22,7 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
[nameof(IPicker.TitleColor)] = MapTitleColor, [nameof(IPicker.TitleColor)] = MapTitleColor,
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex, [nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
[nameof(IPicker.TextColor)] = MapTextColor, [nameof(IPicker.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing, [nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment, [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) public static void MapCharacterSpacing(PickerHandler handler, IPicker picker)
{ {
// Character spacing could be implemented with custom text rendering // Character spacing could be implemented with custom text rendering

View File

@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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.Handlers;
using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics;
using SkiaSharp; using SkiaSharp;
@@ -18,7 +20,9 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
{ {
[nameof(IProgress.Progress)] = MapProgress, [nameof(IProgress.Progress)] = MapProgress,
[nameof(IProgress.ProgressColor)] = MapProgressColor, [nameof(IProgress.ProgressColor)] = MapProgressColor,
[nameof(IView.IsEnabled)] = MapIsEnabled,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
}; };
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -39,6 +43,40 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
return new 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) public static void MapProgress(ProgressBarHandler handler, IProgress progress)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
@@ -50,8 +88,19 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (progress.ProgressColor is not null) if (progress.ProgressColor is not null)
{
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor(); 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) 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) if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); 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

@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers; using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.Linux.Hosting;
namespace Microsoft.Maui.Platform.Linux.Handlers; 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 // Create handler for content if it doesn't exist
if (content.Handler == null) if (content.Handler == null)
{ {
content.Handler = content.ToHandler(handler.MauiContext); content.Handler = content.ToViewHandler(handler.MauiContext);
} }
if (content.Handler?.PlatformView is SkiaView skiaContent) if (content.Handler?.PlatformView is SkiaView skiaContent)

View File

@@ -19,6 +19,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
[nameof(ISwitch.TrackColor)] = MapTrackColor, [nameof(ISwitch.TrackColor)] = MapTrackColor,
[nameof(ISwitch.ThumbColor)] = MapThumbColor, [nameof(ISwitch.ThumbColor)] = MapThumbColor,
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
[nameof(IView.IsEnabled)] = MapIsEnabled,
}; };
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper) 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(); 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

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Animations; using Microsoft.Maui.Animations;
using Microsoft.Maui.Dispatching; using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Dispatching;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Hosting;
@@ -82,125 +83,6 @@ public class ScopedLinuxMauiContext : IMauiContext
public IMauiHandlersFactory Handlers => _parent.Handlers; 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> /// <summary>
/// Linux animation manager. /// Linux animation manager.
/// </summary> /// </summary>

View File

@@ -476,22 +476,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

@@ -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 WebViewHandler(),
[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 FlexLayoutHandler(),
[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

@@ -1,12 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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.Extensions.DependencyInjection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Hosting; 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.Rendering;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux; namespace Microsoft.Maui.Platform.Linux;
@@ -15,19 +27,114 @@ namespace Microsoft.Maui.Platform.Linux;
/// </summary> /// </summary>
public class LinuxApplication : IDisposable 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 X11Window? _mainWindow;
private GtkHostWindow? _gtkWindow;
private SkiaRenderingEngine? _renderingEngine; private SkiaRenderingEngine? _renderingEngine;
private SkiaView? _rootView; private SkiaView? _rootView;
private SkiaView? _focusedView; private SkiaView? _focusedView;
private SkiaView? _hoveredView; private SkiaView? _hoveredView;
private SkiaView? _capturedView; // View that has captured pointer events during drag private SkiaView? _capturedView; // View that has captured pointer events during drag
private bool _disposed; private bool _disposed;
private bool _useGtk;
/// <summary> /// <summary>
/// Gets the current application instance. /// Gets the current application instance.
/// </summary> /// </summary>
public static LinuxApplication? Current { get; private set; } 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> /// <summary>
/// Gets the main window. /// Gets the main window.
/// </summary> /// </summary>
@@ -112,41 +219,55 @@ public class LinuxApplication : IDisposable
/// <param name="configure">Optional configuration action.</param> /// <param name="configure">Optional configuration action.</param>
public static void Run(MauiApp app, string[] args, Action<LinuxApplicationOptions>? configure) 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>() var options = app.Services.GetService<LinuxApplicationOptions>()
?? new LinuxApplicationOptions(); ?? new LinuxApplicationOptions();
configure?.Invoke(options); configure?.Invoke(options);
ParseCommandLineOptions(args, options); ParseCommandLineOptions(args, options);
using var linuxApp = new LinuxApplication(); var linuxApp = new LinuxApplication();
try
{
linuxApp.Initialize(options); linuxApp.Initialize(options);
// Create MAUI context // Create MAUI context
var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp); var mauiContext = new LinuxMauiContext(app.Services, linuxApp);
// Get the application and render it // Get the application and render it
var application = app.Services.GetService<IApplication>(); var application = app.Services.GetService<IApplication>();
SkiaView? rootView = null; SkiaView? rootView = null;
if (application is Microsoft.Maui.Controls.Application mauiApplication) if (application is Application mauiApplication)
{ {
// Force Application.Current to be this instance // Force Application.Current to be this instance
// The constructor sets Current = this, but we ensure it here var currentProperty = typeof(Application).GetProperty("Current");
var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current");
if (currentProperty != null && currentProperty.CanWrite) if (currentProperty != null && currentProperty.CanWrite)
{ {
currentProperty.SetValue(null, mauiApplication); 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) if (mauiApplication.MainPage != 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; var mainPage = mauiApplication.MainPage;
// Always ensure we have a window with the Shell/Page var windowsField = typeof(Application).GetField("_windows",
var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows", BindingFlags.NonPublic | BindingFlags.Instance);
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List<Microsoft.Maui.Controls.Window>;
if (windowsList != null && windowsList.Count == 0) if (windowsList != null && windowsList.Count == 0)
{ {
@@ -156,21 +277,18 @@ public class LinuxApplication : IDisposable
} }
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null) else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
{ {
// Window exists but has no page - set it
windowsList[0].Page = mainPage; windowsList[0].Page = mainPage;
} }
var renderer = new Hosting.LinuxViewRenderer(mauiContext); var renderer = new LinuxViewRenderer(mauiContext);
rootView = renderer.RenderPage(mainPage); rootView = renderer.RenderPage(mainPage);
// Update window title based on app name (NavigationPage.Title takes precedence)
string windowTitle = "OpenMaui App"; string windowTitle = "OpenMaui App";
if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage) if (mainPage is NavigationPage navPage)
{ {
// Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title
windowTitle = navPage.Title ?? windowTitle; windowTitle = navPage.Title ?? windowTitle;
} }
else if (mainPage is Microsoft.Maui.Controls.Shell shell) else if (mainPage is Shell shell)
{ {
windowTitle = shell.Title ?? windowTitle; windowTitle = shell.Title ?? windowTitle;
} }
@@ -182,15 +300,19 @@ public class LinuxApplication : IDisposable
} }
} }
// Fallback to demo if no view
if (rootView == null) if (rootView == null)
{ {
rootView = Hosting.LinuxProgramHost.CreateDemoView(); rootView = LinuxProgramHost.CreateDemoView();
} }
linuxApp.RootView = rootView; linuxApp.RootView = rootView;
linuxApp.Run(); linuxApp.Run();
} }
finally
{
linuxApp?.Dispose();
}
}
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
{ {
@@ -218,16 +340,37 @@ public class LinuxApplication : IDisposable
/// </summary> /// </summary>
public void Initialize(LinuxApplicationOptions options) 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( _mainWindow = new X11Window(
options.Title ?? "MAUI Application", options.Title ?? "MAUI Application",
options.Width, options.Width,
options.Height); 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); _renderingEngine = new SkiaRenderingEngine(_mainWindow);
// Wire up events
_mainWindow.Resized += OnWindowResized; _mainWindow.Resized += OnWindowResized;
_mainWindow.Exposed += OnWindowExposed; _mainWindow.Exposed += OnWindowExposed;
_mainWindow.KeyDown += OnKeyDown; _mainWindow.KeyDown += OnKeyDown;
@@ -238,9 +381,69 @@ public class LinuxApplication : IDisposable
_mainWindow.PointerReleased += OnPointerReleased; _mainWindow.PointerReleased += OnPointerReleased;
_mainWindow.Scroll += OnScroll; _mainWindow.Scroll += OnScroll;
_mainWindow.CloseRequested += OnCloseRequested; _mainWindow.CloseRequested += OnCloseRequested;
}
// Register platform services private void InitializeGtk(LinuxApplicationOptions options)
RegisterServices(); {
_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() private void RegisterServices()
@@ -261,27 +464,62 @@ public class LinuxApplication : IDisposable
/// Shows the main window and runs the event loop. /// Shows the main window and runs the event loop.
/// </summary> /// </summary>
public void Run() public void Run()
{
if (_useGtk)
{
RunGtk();
}
else
{
RunX11();
}
}
private void RunX11()
{ {
if (_mainWindow == null) if (_mainWindow == null)
throw new InvalidOperationException("Application not initialized"); throw new InvalidOperationException("Application not initialized");
_mainWindow.Show(); _mainWindow.Show();
// Initial render
Render(); Render();
// Run the event loop Console.WriteLine("[LinuxApplication] Starting event loop");
while (_mainWindow.IsRunning) 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(); UpdateAnimations();
Render(); Render();
// Small delay to prevent 100% CPU usage
Thread.Sleep(1); 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() private void UpdateAnimations()
@@ -358,6 +596,13 @@ public class LinuxApplication : IDisposable
private void OnPointerMoved(object? sender, PointerEventArgs e) private void OnPointerMoved(object? sender, PointerEventArgs e)
{ {
// Route to context menu if one is active
if (LinuxDialogService.HasContextMenu)
{
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e);
return;
}
// Route to dialog if one is active // Route to dialog if one is active
if (LinuxDialogService.HasActiveDialog) if (LinuxDialogService.HasActiveDialog)
{ {
@@ -384,6 +629,10 @@ public class LinuxApplication : IDisposable
_hoveredView?.OnPointerExited(e); _hoveredView?.OnPointerExited(e);
_hoveredView = hitView; _hoveredView = hitView;
_hoveredView?.OnPointerEntered(e); _hoveredView?.OnPointerEntered(e);
// Update cursor based on view's cursor type
CursorType cursor = hitView?.CursorType ?? CursorType.Arrow;
_mainWindow?.SetCursor(cursor);
} }
hitView?.OnPointerMoved(e); hitView?.OnPointerMoved(e);
@@ -394,6 +643,13 @@ public class LinuxApplication : IDisposable
{ {
Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})"); 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 // Route to dialog if one is active
if (LinuxDialogService.HasActiveDialog) if (LinuxDialogService.HasActiveDialog)
{ {
@@ -489,6 +745,224 @@ public class LinuxApplication : IDisposable
_mainWindow?.Stop(); _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() public void Dispose()
{ {
if (!_disposed) if (!_disposed)
@@ -538,6 +1012,16 @@ public class LinuxApplicationOptions
/// Gets or sets whether to force demo mode instead of loading the application's pages. /// Gets or sets whether to force demo mode instead of loading the application's pages.
/// </summary> /// </summary>
public bool ForceDemo { get; set; } = false; public bool ForceDemo { get; set; } = false;
/// <summary>
/// Gets or sets whether to use GTK mode instead of X11.
/// </summary>
public bool UseGtk { get; set; } = false;
/// <summary>
/// Gets or sets the path to the application icon.
/// </summary>
public string? IconPath { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -4,17 +4,20 @@
| Category | In Main | In Decompiled | New to Add | To Compare | Completed | | Category | In Main | In Decompiled | New to Add | To Compare | Completed |
|----------|---------|---------------|------------|------------|-----------| |----------|---------|---------------|------------|------------|-----------|
| Handlers | 44 | 48 | 13 | 35 | 0 | | Handlers | 44 | 48 | 13 | 35 | 23 |
| Views/Types | 41 | 118 | 77 | 41 | 0 | | Views/Types | 41 | 118 | 77 | 41 | 10 |
| Services | 33 | 103 | 70 | 33 | 0 | | Services | 33 | 103 | 70 | 33 | 7 |
| Hosting | 5 | 12 | 7 | 5 | 0 | | Hosting | 5 | 12 | 7 | 5 | 2 |
| Dispatching | 0 | 3 | 3 | 0 | 0 | | Dispatching | 0 | 3 | 3 | 0 | 3 |
| Native | 0 | 5 | 5 | 0 | 0 | | Native | 0 | 5 | 5 | 0 | 5 |
| **TOTAL** | **123** | **289** | **175** | **114** | **0** | | Window | 2 | 3 | 1 | 2 | 3 |
| Rendering | 1 | 2 | 1 | 1 | 1 |
| **TOTAL** | **123** | **289** | **175** | **114** | **54** |
**Branch:** `final` **Branch:** `main`
**Base:** Clean main (builds with 0 errors) **Base:** Clean main (builds with 0 errors)
**Status:** Ready to begin **Status:** In progress - BUILD SUCCEEDS
**Last Updated:** 2026-01-01
--- ---
@@ -22,62 +25,62 @@
### New Handlers (13 files) - TO ADD ### New Handlers (13 files) - TO ADD
- [ ] ContentPageHandler.cs - [ ] ContentPageHandler.cs - EXISTS IN PageHandler.cs, needs comparison
- [ ] FlexLayoutHandler.cs - [x] FlexLayoutHandler.cs - ADDED
- [ ] GestureManager.cs - [x] GestureManager.cs - ADDED (tap, pan, swipe, pointer gesture processing)
- [ ] GridHandler.cs - [ ] GridHandler.cs - EXISTS IN LayoutHandler.cs, needs comparison
- [ ] GtkWebViewHandler.cs - [ ] GtkWebViewHandler.cs
- [ ] GtkWebViewManager.cs - [x] GtkWebViewManager.cs - ADDED
- [ ] GtkWebViewPlatformView.cs - [x] GtkWebViewPlatformView.cs - ADDED
- [ ] GtkWebViewProxy.cs - [ ] GtkWebViewProxy.cs
- [ ] LayoutHandlerUpdate.cs - [ ] LayoutHandlerUpdate.cs - EXISTS IN LayoutHandler.cs
- [ ] LinuxApplicationContext.cs - [ ] LinuxApplicationContext.cs
- [ ] RelayCommand.cs - [ ] RelayCommand.cs - EXISTS IN NavigationPageHandler.cs
- [ ] SizeChangedEventArgs.cs - [ ] SizeChangedEventArgs.cs
- [ ] SkiaWindow.cs - [ ] SkiaWindow.cs
- [ ] StackLayoutHandler.cs - [ ] StackLayoutHandler.cs - EXISTS IN LayoutHandler.cs, needs comparison
- [ ] TextButtonHandler.cs - [ ] TextButtonHandler.cs - EXISTS IN ButtonHandler.cs
### Existing Handlers (35 files) - TO COMPARE ### Existing Handlers (35 files) - TO COMPARE
- [ ] ActivityIndicatorHandler.cs - [ ] ActivityIndicatorHandler.cs
- [ ] ActivityIndicatorHandler.Linux.cs - [ ] ActivityIndicatorHandler.Linux.cs
- [ ] ApplicationHandler.cs - [ ] ApplicationHandler.cs
- [ ] BorderHandler.cs - [x] BorderHandler.cs - Updated to use ToViewHandler
- [ ] BoxViewHandler.cs - [ ] BoxViewHandler.cs
- [ ] ButtonHandler.cs - [ ] ButtonHandler.cs
- [ ] ButtonHandler.Linux.cs - [ ] ButtonHandler.Linux.cs
- [ ] CheckBoxHandler.cs - [x] CheckBoxHandler.cs - Updated with missing mappers
- [ ] CheckBoxHandler.Linux.cs - [ ] CheckBoxHandler.Linux.cs
- [ ] CollectionViewHandler.cs - [x] CollectionViewHandler.cs - Updated to use ToViewHandler
- [ ] DatePickerHandler.cs - [x] DatePickerHandler.cs - Updated with missing mappers
- [ ] EditorHandler.cs - [ ] EditorHandler.cs
- [ ] EntryHandler.cs - [x] EntryHandler.cs - Updated with missing mappers
- [ ] EntryHandler.Linux.cs - [ ] EntryHandler.Linux.cs
- [ ] FlyoutPageHandler.cs - [ ] FlyoutPageHandler.cs
- [ ] FrameHandler.cs - [x] FrameHandler.cs - Updated to use ToViewHandler
- [ ] GraphicsViewHandler.cs - [ ] GraphicsViewHandler.cs
- [ ] ImageButtonHandler.cs - [ ] ImageButtonHandler.cs
- [ ] ImageHandler.cs - [x] ImageHandler.cs - Updated with LoadFromBitmap support
- [ ] ItemsViewHandler.cs - [ ] ItemsViewHandler.cs
- [ ] LabelHandler.cs - [x] LabelHandler.cs - Added ConnectHandler, DisconnectHandler, OnPlatformViewTapped, MapFormattedText
- [ ] LabelHandler.Linux.cs - [ ] LabelHandler.Linux.cs
- [ ] LayoutHandler.cs - [x] LayoutHandler.cs - Updated to use ToViewHandler
- [ ] LayoutHandler.Linux.cs - [x] LayoutHandler.Linux.cs - Updated to use ToViewHandler
- [ ] NavigationPageHandler.cs - [x] NavigationPageHandler.cs - Updated to use ToViewHandler
- [ ] PageHandler.cs - [x] PageHandler.cs - Updated to use ToViewHandler
- [ ] PickerHandler.cs - [x] PickerHandler.cs - Updated with missing mappers
- [ ] ProgressBarHandler.cs - [x] ProgressBarHandler.cs - Updated with missing mappers
- [ ] ProgressBarHandler.Linux.cs - [ ] ProgressBarHandler.Linux.cs
- [ ] RadioButtonHandler.cs - [ ] RadioButtonHandler.cs
- [ ] ScrollViewHandler.cs - [x] ScrollViewHandler.cs - Updated to use ToViewHandler
- [ ] SearchBarHandler.cs - [ ] SearchBarHandler.cs
- [ ] SearchBarHandler.Linux.cs - [ ] SearchBarHandler.Linux.cs
- [ ] ShellHandler.cs - [ ] ShellHandler.cs
- [ ] SliderHandler.cs - [ ] SliderHandler.cs
- [ ] SliderHandler.Linux.cs - [ ] SliderHandler.Linux.cs
- [ ] StepperHandler.cs - [ ] StepperHandler.cs
- [ ] SwitchHandler.cs - [x] SwitchHandler.cs - Updated with missing mappers
- [ ] SwitchHandler.Linux.cs - [ ] SwitchHandler.Linux.cs
- [ ] TabbedPageHandler.cs - [ ] TabbedPageHandler.cs
- [ ] TimePickerHandler.cs - [ ] TimePickerHandler.cs
@@ -91,72 +94,72 @@
### New Types (77 files) - TO ADD ### New Types (77 files) - TO ADD
- [ ] AbsoluteLayoutBounds.cs - [ ] AbsoluteLayoutBounds.cs - EXISTS IN SkiaLayoutView.cs
- [ ] AbsoluteLayoutFlags.cs - [ ] AbsoluteLayoutFlags.cs - EXISTS IN SkiaLayoutView.cs
- [ ] CheckedChangedEventArgs.cs - [ ] CheckedChangedEventArgs.cs
- [ ] CollectionSelectionChangedEventArgs.cs - [ ] CollectionSelectionChangedEventArgs.cs
- [ ] ColorExtensions.cs - [ ] ColorExtensions.cs
- [ ] ContextMenuItem.cs - [ ] ContextMenuItem.cs - EXISTS IN Types/
- [ ] FlexAlignContent.cs - [ ] FlexAlignContent.cs - EXISTS IN Types/
- [ ] FlexAlignItems.cs - [ ] FlexAlignItems.cs - EXISTS IN Types/
- [ ] FlexAlignSelf.cs - [ ] FlexAlignSelf.cs - EXISTS IN Types/
- [ ] FlexBasis.cs - [ ] FlexBasis.cs - EXISTS IN Types/
- [ ] FlexDirection.cs - [ ] FlexDirection.cs - EXISTS IN Types/
- [ ] FlexJustify.cs - [ ] FlexJustify.cs - EXISTS IN Types/
- [ ] FlexWrap.cs - [ ] FlexWrap.cs - EXISTS IN Types/
- [ ] FlyoutLayoutBehavior.cs - [ ] FlyoutLayoutBehavior.cs
- [ ] FontExtensions.cs - [ ] FontExtensions.cs
- [ ] GridLength.cs - [ ] GridLength.cs - EXISTS IN SkiaLayoutView.cs
- [ ] GridPosition.cs - [ ] GridPosition.cs - EXISTS IN SkiaLayoutView.cs
- [ ] GridUnitType.cs - [ ] GridUnitType.cs - EXISTS IN SkiaLayoutView.cs
- [ ] ImageLoadingErrorEventArgs.cs - [ ] ImageLoadingErrorEventArgs.cs
- [ ] IndicatorShape.cs - [ ] IndicatorShape.cs
- [ ] ISkiaQueryAttributable.cs - [ ] ISkiaQueryAttributable.cs - EXISTS IN Types/
- [ ] ItemsLayoutOrientation.cs - [ ] ItemsLayoutOrientation.cs
- [ ] ItemsScrolledEventArgs.cs - [ ] ItemsScrolledEventArgs.cs
- [ ] ItemsViewItemTappedEventArgs.cs - [ ] ItemsViewItemTappedEventArgs.cs
- [ ] Key.cs - [ ] Key.cs - EXISTS IN SkiaView.cs
- [ ] KeyEventArgs.cs - [ ] KeyEventArgs.cs - EXISTS IN SkiaView.cs
- [ ] KeyModifiers.cs - [ ] KeyModifiers.cs - EXISTS IN SkiaView.cs
- [ ] LayoutAlignment.cs - [ ] LayoutAlignment.cs
- [ ] LineBreakMode.cs - [ ] LineBreakMode.cs
- [ ] LinuxDialogService.cs - [ ] LinuxDialogService.cs
- [ ] MenuBarItem.cs - [ ] MenuBarItem.cs - EXISTS IN SkiaMenuBar.cs
- [ ] MenuItem.cs - [ ] MenuItem.cs - EXISTS IN SkiaMenuBar.cs
- [ ] MenuItemClickedEventArgs.cs - [ ] MenuItemClickedEventArgs.cs - EXISTS IN SkiaMenuBar.cs
- [ ] NavigationEventArgs.cs - [ ] NavigationEventArgs.cs - EXISTS IN SkiaNavigationPage.cs
- [ ] PointerButton.cs - [ ] PointerButton.cs - EXISTS IN SkiaView.cs
- [ ] PointerEventArgs.cs - [ ] PointerEventArgs.cs - EXISTS IN SkiaView.cs
- [ ] PositionChangedEventArgs.cs - [ ] PositionChangedEventArgs.cs
- [ ] ProgressChangedEventArgs.cs - [ ] ProgressChangedEventArgs.cs
- [ ] ScrollBarVisibility.cs - [ ] ScrollBarVisibility.cs - EXISTS IN SkiaScrollView.cs
- [ ] ScrolledEventArgs.cs - [ ] ScrolledEventArgs.cs - EXISTS IN SkiaScrollView.cs
- [ ] ScrollEventArgs.cs - [ ] ScrollEventArgs.cs - EXISTS IN SkiaView.cs
- [ ] ScrollOrientation.cs - [ ] ScrollOrientation.cs - EXISTS IN SkiaScrollView.cs
- [ ] ShellContent.cs - [ ] ShellContent.cs - EXISTS IN SkiaShell.cs
- [ ] ShellFlyoutBehavior.cs - [ ] ShellFlyoutBehavior.cs - EXISTS IN SkiaShell.cs
- [ ] ShellNavigationEventArgs.cs - [ ] ShellNavigationEventArgs.cs - EXISTS IN SkiaShell.cs
- [ ] ShellSection.cs - [ ] ShellSection.cs - EXISTS IN SkiaShell.cs
- [ ] SkiaAbsoluteLayout.cs - [ ] SkiaAbsoluteLayout.cs - EXISTS IN SkiaLayoutView.cs
- [ ] SkiaContentPage.cs - [ ] SkiaContentPage.cs - EXISTS IN SkiaPage.cs
- [ ] SkiaContextMenu.cs - [ ] SkiaContextMenu.cs
- [ ] SkiaFlexLayout.cs - [x] SkiaFlexLayout.cs - ADDED
- [ ] SkiaFrame.cs - [ ] SkiaFrame.cs - EXISTS IN SkiaBorder.cs
- [ ] SkiaGrid.cs - [ ] SkiaGrid.cs - EXISTS IN SkiaLayoutView.cs
- [ ] SkiaMenuFlyout.cs - [ ] SkiaMenuFlyout.cs
- [ ] SkiaSelectionMode.cs - [ ] SkiaSelectionMode.cs
- [ ] SkiaStackLayout.cs - [ ] SkiaStackLayout.cs - EXISTS IN SkiaLayoutView.cs
- [ ] SkiaTextAlignment.cs - [ ] SkiaTextAlignment.cs
- [ ] SkiaTextSpan.cs - [ ] SkiaTextSpan.cs - EXISTS IN Types/
- [ ] SkiaToolbarItem.cs - [ ] SkiaToolbarItem.cs - EXISTS IN SkiaPage.cs
- [ ] SkiaToolbarItemOrder.cs - [ ] SkiaToolbarItemOrder.cs - EXISTS IN SkiaPage.cs
- [ ] SkiaVerticalAlignment.cs - [ ] SkiaVerticalAlignment.cs
- [ ] SkiaVisualState.cs - [ ] SkiaVisualState.cs
- [ ] SkiaVisualStateGroup.cs - [ ] SkiaVisualStateGroup.cs
- [ ] SkiaVisualStateGroupList.cs - [ ] SkiaVisualStateGroupList.cs
- [ ] SkiaVisualStateSetter.cs - [ ] SkiaVisualStateSetter.cs
- [ ] SliderValueChangedEventArgs.cs - [ ] SliderValueChangedEventArgs.cs
- [ ] StackOrientation.cs - [ ] StackOrientation.cs - EXISTS IN SkiaLayoutView.cs
- [ ] SwipeDirection.cs - [ ] SwipeDirection.cs
- [ ] SwipeEndedEventArgs.cs - [ ] SwipeEndedEventArgs.cs
- [ ] SwipeItem.cs - [ ] SwipeItem.cs
@@ -166,7 +169,7 @@
- [ ] TabItem.cs - [ ] TabItem.cs
- [ ] TextAlignment.cs - [ ] TextAlignment.cs
- [ ] TextChangedEventArgs.cs - [ ] TextChangedEventArgs.cs
- [ ] TextInputEventArgs.cs - [ ] TextInputEventArgs.cs - EXISTS IN SkiaView.cs
- [ ] ThicknessExtensions.cs - [ ] ThicknessExtensions.cs
- [ ] ToggledEventArgs.cs - [ ] ToggledEventArgs.cs
- [ ] WebNavigatedEventArgs.cs - [ ] WebNavigatedEventArgs.cs
@@ -186,14 +189,14 @@
- [ ] SkiaContentPresenter.cs - [ ] SkiaContentPresenter.cs
- [ ] SkiaDatePicker.cs - [ ] SkiaDatePicker.cs
- [ ] SkiaEditor.cs - [ ] SkiaEditor.cs
- [ ] SkiaEntry.cs - [x] SkiaEntry.cs - Added context menu support
- [ ] SkiaFlyoutPage.cs - [ ] SkiaFlyoutPage.cs
- [ ] SkiaGraphicsView.cs - [ ] SkiaGraphicsView.cs
- [ ] SkiaImage.cs - [x] SkiaImage.cs - Added LoadFromBitmap method
- [ ] SkiaImageButton.cs - [ ] SkiaImageButton.cs
- [ ] SkiaIndicatorView.cs - [ ] SkiaIndicatorView.cs
- [ ] SkiaItemsView.cs - [ ] SkiaItemsView.cs
- [ ] SkiaLabel.cs - [x] SkiaLabel.cs - Added FormattedSpans, Tapped event, formatted text rendering
- [ ] SkiaLayoutView.cs - [ ] SkiaLayoutView.cs
- [ ] SkiaMenuBar.cs - [ ] SkiaMenuBar.cs
- [ ] SkiaNavigationPage.cs - [ ] SkiaNavigationPage.cs
@@ -204,7 +207,7 @@
- [ ] SkiaRefreshView.cs - [ ] SkiaRefreshView.cs
- [ ] SkiaScrollView.cs - [ ] SkiaScrollView.cs
- [ ] SkiaSearchBar.cs - [ ] SkiaSearchBar.cs
- [ ] SkiaShell.cs - [x] SkiaShell.cs - Added MauiShell, ContentRenderer, ColorRefresher, RefreshTheme()
- [ ] SkiaSlider.cs - [ ] SkiaSlider.cs
- [ ] SkiaStepper.cs - [ ] SkiaStepper.cs
- [ ] SkiaSwipeView.cs - [ ] SkiaSwipeView.cs
@@ -212,9 +215,14 @@
- [ ] SkiaTabbedPage.cs - [ ] SkiaTabbedPage.cs
- [ ] SkiaTemplatedView.cs - [ ] SkiaTemplatedView.cs
- [ ] SkiaTimePicker.cs - [ ] SkiaTimePicker.cs
- [ ] SkiaView.cs - [x] SkiaView.cs - Added MauiView, CursorType, transforms (Scale/Rotation/Translation/Anchor), GestureManager integration, enhanced Invalidate/Draw
- [ ] SkiaVisualStateManager.cs - [ ] SkiaVisualStateManager.cs
- [ ] SkiaWebView.cs - [x] SkiaWebView.cs - Added SetMainWindow, ProcessGtkEvents static methods
### New Views Added This Session
- [x] SkiaContextMenu.cs - ADDED (context menu with hover, keyboard, dark theme support)
- [x] LinuxDialogService.cs - ADDED (dialog and context menu management)
--- ---
@@ -230,13 +238,13 @@
- [ ] AccessibleState.cs - [ ] AccessibleState.cs
- [ ] AccessibleStates.cs - [ ] AccessibleStates.cs
- [ ] AnnouncementPriority.cs - [ ] AnnouncementPriority.cs
- [ ] AppInfoService.cs - [x] AppInfoService.cs - ADDED
- [ ] ColorDialogResult.cs - [ ] ColorDialogResult.cs
- [ ] ConnectivityService.cs - [x] ConnectivityService.cs - ADDED
- [ ] DesktopEnvironment.cs - [ ] DesktopEnvironment.cs - EXISTS IN SystemThemeService.cs
- [ ] DeviceDisplayService.cs - [x] DeviceDisplayService.cs - ADDED
- [ ] DeviceInfoService.cs - [x] DeviceInfoService.cs - ADDED
- [ ] DisplayServerType.cs - [ ] DisplayServerType.cs - EXISTS IN LinuxApplication.cs
- [ ] DragAction.cs - [ ] DragAction.cs
- [ ] DragData.cs - [ ] DragData.cs
- [ ] DragEventArgs.cs - [ ] DragEventArgs.cs
@@ -248,7 +256,7 @@
- [ ] GtkButtonsType.cs - [ ] GtkButtonsType.cs
- [ ] GtkContextMenuService.cs - [ ] GtkContextMenuService.cs
- [ ] GtkFileChooserAction.cs - [ ] GtkFileChooserAction.cs
- [ ] GtkHostService.cs - [x] GtkHostService.cs - ADDED
- [ ] GtkMenuItem.cs - [ ] GtkMenuItem.cs
- [ ] GtkMessageType.cs - [ ] GtkMessageType.cs
- [ ] GtkResponseType.cs - [ ] GtkResponseType.cs
@@ -265,7 +273,7 @@
- [ ] IInputContext.cs - [ ] IInputContext.cs
- [ ] KeyModifiers.cs - [ ] KeyModifiers.cs
- [ ] LinuxFileResult.cs - [ ] LinuxFileResult.cs
- [ ] MauiIconGenerator.cs - [x] MauiIconGenerator.cs - ADDED (PNG icon generator, no Svg.Skia dependency)
- [ ] NotificationAction.cs - [ ] NotificationAction.cs
- [ ] NotificationActionEventArgs.cs - [ ] NotificationActionEventArgs.cs
- [ ] NotificationClosedEventArgs.cs - [ ] NotificationClosedEventArgs.cs
@@ -337,12 +345,12 @@
- [ ] GtkMauiContext.cs - [ ] GtkMauiContext.cs
- [ ] HandlerMappingExtensions.cs - [ ] HandlerMappingExtensions.cs
- [ ] LinuxAnimationManager.cs - [ ] LinuxAnimationManager.cs - EXISTS IN LinuxMauiContext.cs
- [ ] LinuxDispatcher.cs - [ ] LinuxDispatcher.cs - EXISTS IN LinuxMauiContext.cs
- [ ] LinuxDispatcherTimer.cs - [ ] LinuxDispatcherTimer.cs - EXISTS IN LinuxMauiContext.cs
- [ ] LinuxTicker.cs - [ ] LinuxTicker.cs - EXISTS IN LinuxMauiContext.cs
- [ ] MauiHandlerExtensions.cs - [x] MauiHandlerExtensions.cs - ADDED (critical ToViewHandler fix)
- [ ] ScopedLinuxMauiContext.cs - [ ] ScopedLinuxMauiContext.cs - EXISTS IN LinuxMauiContext.cs
### Existing Hosting (5 files) - TO COMPARE ### Existing Hosting (5 files) - TO COMPARE
@@ -372,51 +380,39 @@
--- ---
## WINDOW
### Window Files - TO COMPARE/ADD
- [x] CursorType.cs - ADDED (Arrow, Hand, Text cursor types)
- [x] X11Window.cs - Added SetIcon, SetCursor methods, cursor initialization
- [x] GtkHostWindow.cs - ADDED (GTK-based host window with overlay support)
---
## RENDERING
### Rendering Files - TO COMPARE/ADD
- [x] GtkSkiaSurfaceWidget.cs - ADDED (GTK drawing area for Skia)
---
## CORE FILES ## CORE FILES
### Core (2 files) - TO COMPARE ### Core (2 files) - TO COMPARE
- [ ] LinuxApplication.cs - [x] LinuxApplication.cs - Massive update: GTK mode, Dispatcher init, theme handling, icon support, GTK events
- [ ] LinuxApplicationOptions.cs - [ ] LinuxApplicationOptions.cs
--- ---
## Progress Log ## HOSTING
| Date | Files Completed | Notes | ### Hosting Files - TO COMPARE/ADD
|------|-----------------|-------|
| 2026-01-01 | 10 Types | Added FlexDirection, FlexWrap, FlexJustify, FlexAlignItems, FlexAlignContent, FlexAlignSelf, FlexBasis, ContextMenuItem, ISkiaQueryAttributable, SkiaTextSpan |
| 2026-01-01 | 1 Handler | Added FlexLayoutHandler.cs |
| 2026-01-01 | 1 View | Added SkiaFlexLayout.cs |
--- - [x] LinuxMauiContext.cs - Fixed duplicate LinuxDispatcher, uses Dispatching namespace
- [x] MauiHandlerExtensions.cs - ADDED (ToViewHandler extension)
## ⚠️ INCORRECTLY SKIPPED - MUST COMPARE AND UPDATE
These were WRONGLY skipped because I assumed main was correct. Main is OUTDATED - decompiled has the production fixes.
### Files that need COMPARISON (not skipped):
**Handlers to compare (embedded in other files):**
- [ ] GridHandler - exists in LayoutHandler.cs, COMPARE with decompiled GridHandler.cs
- [ ] StackLayoutHandler - exists in LayoutHandler.cs, COMPARE with decompiled StackLayoutHandler.cs
- [ ] ContentPageHandler - exists in PageHandler.cs, COMPARE with decompiled ContentPageHandler.cs
**Views to compare (embedded in other files):**
- [ ] SkiaGrid - exists in SkiaLayoutView.cs, COMPARE with decompiled SkiaGrid.cs
- [ ] SkiaStackLayout - exists in SkiaLayoutView.cs, COMPARE with decompiled SkiaStackLayout.cs
- [ ] SkiaAbsoluteLayout - exists in SkiaLayoutView.cs, COMPARE with decompiled SkiaAbsoluteLayout.cs
- [ ] SkiaContentPage - exists in SkiaPage.cs, COMPARE with decompiled SkiaContentPage.cs
- [ ] SkiaFrame - exists in SkiaBorder.cs, COMPARE with decompiled SkiaFrame.cs
- [ ] SkiaContextMenu - exists in SkiaMenuBar.cs(?), COMPARE with decompiled
- [ ] SkiaMenuFlyout - exists in SkiaMenuBar.cs, COMPARE with decompiled
**Types to compare (embedded in View files):**
- [ ] All types in SkiaView.cs (KeyEventArgs, PointerEventArgs, ScrollEventArgs, TextInputEventArgs, Key, KeyModifiers, PointerButton)
- [ ] Types in SkiaLayoutView.cs (GridLength, GridPosition, AbsoluteLayoutBounds, AbsoluteLayoutFlags, GridUnitType, StackOrientation)
- [ ] Types in SkiaMenuBar.cs (MenuItem, MenuBarItem, MenuItemClickedEventArgs)
- [ ] Types in SkiaShell.cs (ShellSection, ShellContent, ShellNavigationEventArgs, ShellFlyoutBehavior)
- [ ] And many more...
--- ---

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

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

142
Services/AppInfoService.cs Normal file
View File

@@ -0,0 +1,142 @@
using System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Platform.Linux.Services;
public class AppInfoService : IAppInfo
{
private static readonly Lazy<AppInfoService> _instance = new Lazy<AppInfoService>(() => new AppInfoService());
private readonly Assembly _entryAssembly;
private readonly string _packageName;
private readonly string _name;
private readonly string _versionString;
private readonly Version _version;
private readonly string _buildString;
public static AppInfoService Instance => _instance.Value;
public string PackageName => _packageName;
public string Name => _name;
public string VersionString => _versionString;
public Version Version => _version;
public string BuildString => _buildString;
public LayoutDirection RequestedLayoutDirection => LayoutDirection.LeftToRight;
public AppTheme RequestedTheme
{
get
{
try
{
string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME");
if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("dark", StringComparison.OrdinalIgnoreCase))
{
return AppTheme.Dark;
}
if (GetGnomeColorScheme().Contains("dark", StringComparison.OrdinalIgnoreCase))
{
return AppTheme.Dark;
}
return AppTheme.Light;
}
catch
{
return AppTheme.Light;
}
}
}
public AppPackagingModel PackagingModel
{
get
{
if (Environment.GetEnvironmentVariable("FLATPAK_ID") != null)
{
return AppPackagingModel.Packaged;
}
if (Environment.GetEnvironmentVariable("SNAP") != null)
{
return AppPackagingModel.Packaged;
}
if (Environment.GetEnvironmentVariable("APPIMAGE") != null)
{
return AppPackagingModel.Packaged;
}
return AppPackagingModel.Unpackaged;
}
}
public AppInfoService()
{
_entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
_packageName = _entryAssembly.GetName().Name ?? "Unknown";
_name = _entryAssembly.GetCustomAttribute<AssemblyTitleAttribute>()?.Title ?? _packageName;
_versionString = (_version = _entryAssembly.GetName().Version ?? new Version(1, 0)).ToString();
_buildString = _entryAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? _versionString;
}
private string GetGnomeColorScheme()
{
try
{
using Process? process = Process.Start(new ProcessStartInfo
{
FileName = "gsettings",
Arguments = "get org.gnome.desktop.interface color-scheme",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
if (process != null)
{
string text = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return text.Trim().Trim('\'');
}
}
catch
{
}
return "";
}
public void ShowSettingsUI()
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "gnome-control-center",
UseShellExecute = true
});
}
catch
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "x-settings:",
UseShellExecute = true
});
}
catch
{
}
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using Microsoft.Maui.Networking;
namespace Microsoft.Maui.Platform.Linux.Services;
public class ConnectivityService : IConnectivity, IDisposable
{
private static readonly Lazy<ConnectivityService> _instance = new Lazy<ConnectivityService>(() => new ConnectivityService());
private NetworkAccess _networkAccess;
private IEnumerable<ConnectionProfile> _connectionProfiles;
private bool _disposed;
public static ConnectivityService Instance => _instance.Value;
public NetworkAccess NetworkAccess
{
get
{
RefreshConnectivity();
return _networkAccess;
}
}
public IEnumerable<ConnectionProfile> ConnectionProfiles
{
get
{
RefreshConnectivity();
return _connectionProfiles;
}
}
public event EventHandler<ConnectivityChangedEventArgs>? ConnectivityChanged;
public ConnectivityService()
{
_connectionProfiles = new List<ConnectionProfile>();
RefreshConnectivity();
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
}
private void RefreshConnectivity()
{
try
{
IEnumerable<NetworkInterface> activeInterfaces = from ni in NetworkInterface.GetAllNetworkInterfaces()
where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback
select ni;
if (!activeInterfaces.Any())
{
_networkAccess = NetworkAccess.None;
_connectionProfiles = Enumerable.Empty<ConnectionProfile>();
return;
}
List<ConnectionProfile> profiles = new List<ConnectionProfile>();
foreach (var networkInterface in activeInterfaces)
{
switch (networkInterface.NetworkInterfaceType)
{
case NetworkInterfaceType.Ethernet:
case NetworkInterfaceType.FastEthernetT:
case NetworkInterfaceType.FastEthernetFx:
case NetworkInterfaceType.GigabitEthernet:
profiles.Add(ConnectionProfile.Ethernet);
break;
case NetworkInterfaceType.Wireless80211:
profiles.Add(ConnectionProfile.WiFi);
break;
case NetworkInterfaceType.Ppp:
case NetworkInterfaceType.Slip:
profiles.Add(ConnectionProfile.Cellular);
break;
default:
profiles.Add(ConnectionProfile.Unknown);
break;
}
}
_connectionProfiles = profiles.Distinct().ToList();
if (CheckInternetAccess())
{
_networkAccess = NetworkAccess.Internet;
}
else if (_connectionProfiles.Any())
{
_networkAccess = NetworkAccess.Local;
}
else
{
_networkAccess = NetworkAccess.None;
}
}
catch
{
_networkAccess = NetworkAccess.Unknown;
_connectionProfiles = new ConnectionProfile[] { ConnectionProfile.Unknown };
}
}
private bool CheckInternetAccess()
{
try
{
return Dns.GetHostEntry("dns.google").AddressList.Length != 0;
}
catch
{
try
{
foreach (NetworkInterface item in from n in NetworkInterface.GetAllNetworkInterfaces()
where n.OperationalStatus == OperationalStatus.Up
select n)
{
if (item.GetIPProperties().GatewayAddresses.Any((GatewayIPAddressInformation g) => g.Address.AddressFamily == AddressFamily.InterNetwork))
{
return true;
}
}
}
catch
{
}
return false;
}
}
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
{
NetworkAccess previousAccess = _networkAccess;
List<ConnectionProfile> previousProfiles = _connectionProfiles.ToList();
RefreshConnectivity();
if (previousAccess != _networkAccess || !previousProfiles.SequenceEqual(_connectionProfiles))
{
ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
}
}
private void OnNetworkAddressChanged(object? sender, EventArgs e)
{
NetworkAccess previousAccess = _networkAccess;
List<ConnectionProfile> previousProfiles = _connectionProfiles.ToList();
RefreshConnectivity();
if (previousAccess != _networkAccess || !previousProfiles.SequenceEqual(_connectionProfiles))
{
ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Diagnostics;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Platform.Linux.Native;
namespace Microsoft.Maui.Platform.Linux.Services;
public class DeviceDisplayService : IDeviceDisplay
{
private static readonly Lazy<DeviceDisplayService> _instance = new Lazy<DeviceDisplayService>(() => new DeviceDisplayService());
private DisplayInfo _mainDisplayInfo;
private bool _keepScreenOn;
public static DeviceDisplayService Instance => _instance.Value;
public bool KeepScreenOn
{
get
{
return _keepScreenOn;
}
set
{
if (_keepScreenOn != value)
{
_keepScreenOn = value;
SetScreenSaverInhibit(value);
}
}
}
public DisplayInfo MainDisplayInfo
{
get
{
RefreshDisplayInfo();
return _mainDisplayInfo;
}
}
public event EventHandler<DisplayInfoChangedEventArgs>? MainDisplayInfoChanged;
public DeviceDisplayService()
{
RefreshDisplayInfo();
}
private void RefreshDisplayInfo()
{
try
{
IntPtr screen = GdkNative.gdk_screen_get_default();
if (screen != IntPtr.Zero)
{
int width = GdkNative.gdk_screen_get_width(screen);
int height = GdkNative.gdk_screen_get_height(screen);
double scaleFactor = GetScaleFactor();
DisplayOrientation orientation = (width <= height) ? DisplayOrientation.Portrait : DisplayOrientation.Landscape;
_mainDisplayInfo = new DisplayInfo(width, height, scaleFactor, orientation, DisplayRotation.Rotation0, GetRefreshRate());
}
else
{
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, DisplayOrientation.Landscape, DisplayRotation.Rotation0, 60f);
}
}
catch
{
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, DisplayOrientation.Landscape, DisplayRotation.Rotation0, 60f);
}
}
private double GetScaleFactor()
{
string gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE");
if (!string.IsNullOrEmpty(gdkScale) && double.TryParse(gdkScale, out var result))
{
return result;
}
string qtScale = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR");
if (!string.IsNullOrEmpty(qtScale) && double.TryParse(qtScale, out result))
{
return result;
}
return 1.0;
}
private float GetRefreshRate()
{
return 60f;
}
private void SetScreenSaverInhibit(bool inhibit)
{
try
{
string action = inhibit ? "suspend" : "resume";
IntPtr windowHandle = LinuxApplication.Current?.MainWindow?.Handle ?? IntPtr.Zero;
if (windowHandle != IntPtr.Zero)
{
long windowId = windowHandle.ToInt64();
Process.Start(new ProcessStartInfo
{
FileName = "xdg-screensaver",
Arguments = $"{action} {windowId}",
UseShellExecute = false,
CreateNoWindow = true
});
}
}
catch
{
}
}
public void OnDisplayInfoChanged()
{
RefreshDisplayInfo();
MainDisplayInfoChanged?.Invoke(this, new DisplayInfoChangedEventArgs(_mainDisplayInfo));
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.IO;
using Microsoft.Maui.Devices;
namespace Microsoft.Maui.Platform.Linux.Services;
public class DeviceInfoService : IDeviceInfo
{
private static readonly Lazy<DeviceInfoService> _instance = new Lazy<DeviceInfoService>(() => new DeviceInfoService());
private string? _model;
private string? _manufacturer;
private string? _name;
private string? _versionString;
public static DeviceInfoService Instance => _instance.Value;
public string Model => _model ?? "Linux Desktop";
public string Manufacturer => _manufacturer ?? "Unknown";
public string Name => _name ?? Environment.MachineName;
public string VersionString => _versionString ?? Environment.OSVersion.VersionString;
public Version Version
{
get
{
try
{
if (System.Version.TryParse(Environment.OSVersion.Version.ToString(), out Version? result))
{
return result;
}
}
catch
{
}
return new Version(1, 0);
}
}
public DevicePlatform Platform => DevicePlatform.Create("Linux");
public DeviceIdiom Idiom => DeviceIdiom.Desktop;
public DeviceType DeviceType => DeviceType.Physical;
public DeviceInfoService()
{
LoadDeviceInfo();
}
private void LoadDeviceInfo()
{
try
{
if (File.Exists("/sys/class/dmi/id/product_name"))
{
_model = File.ReadAllText("/sys/class/dmi/id/product_name").Trim();
}
if (File.Exists("/sys/class/dmi/id/sys_vendor"))
{
_manufacturer = File.ReadAllText("/sys/class/dmi/id/sys_vendor").Trim();
}
_name = Environment.MachineName;
_versionString = Environment.OSVersion.VersionString;
}
catch
{
if (_model == null)
{
_model = "Linux Desktop";
}
if (_manufacturer == null)
{
_manufacturer = "Unknown";
}
if (_name == null)
{
_name = "localhost";
}
if (_versionString == null)
{
_versionString = "Linux";
}
}
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Native;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Service for displaying native GTK context menus in MAUI applications.
/// Provides popup menu functionality with action callbacks.
/// </summary>
public static class GtkContextMenuService
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ActivateCallback(IntPtr menuItem, IntPtr userData);
// Keep references to prevent garbage collection
private static readonly List<ActivateCallback> _callbacks = new();
private static readonly List<Action> _actions = new();
public static void ShowContextMenu(List<GtkMenuItem> items)
{
if (items == null || items.Count == 0)
{
return;
}
_callbacks.Clear();
_actions.Clear();
IntPtr menu = GtkNative.gtk_menu_new();
if (menu == IntPtr.Zero)
{
Console.WriteLine("[GtkContextMenuService] Failed to create GTK menu");
return;
}
foreach (var item in items)
{
IntPtr menuItem;
if (item.IsSeparator)
{
menuItem = GtkNative.gtk_separator_menu_item_new();
}
else
{
menuItem = GtkNative.gtk_menu_item_new_with_label(item.Text);
GtkNative.gtk_widget_set_sensitive(menuItem, item.IsEnabled);
if (item.IsEnabled && item.Action != null)
{
var action = item.Action;
_actions.Add(action);
int actionIndex = _actions.Count - 1;
ActivateCallback callback = delegate
{
Console.WriteLine("[GtkContextMenuService] Menu item activated: " + item.Text);
_actions[actionIndex]?.Invoke();
};
_callbacks.Add(callback);
GtkNative.g_signal_connect_data(
menuItem,
"activate",
Marshal.GetFunctionPointerForDelegate(callback),
IntPtr.Zero,
IntPtr.Zero,
0);
}
}
GtkNative.gtk_menu_shell_append(menu, menuItem);
GtkNative.gtk_widget_show(menuItem);
}
GtkNative.gtk_widget_show(menu);
IntPtr currentEvent = GtkNative.gtk_get_current_event();
GtkNative.gtk_menu_popup_at_pointer(menu, currentEvent);
if (currentEvent != IntPtr.Zero)
{
GtkNative.gdk_event_free(currentEvent);
}
Console.WriteLine($"[GtkContextMenuService] Showed GTK menu with {items.Count} items");
}
}

View File

@@ -0,0 +1,56 @@
using System;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Window;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Singleton service that manages the GTK host window and WebView manager.
/// Provides centralized access to the GTK infrastructure for MAUI applications.
/// </summary>
public class GtkHostService
{
private static GtkHostService? _instance;
private GtkHostWindow? _hostWindow;
private GtkWebViewManager? _webViewManager;
public static GtkHostService Instance => _instance ??= new GtkHostService();
public GtkHostWindow? HostWindow => _hostWindow;
public GtkWebViewManager? WebViewManager => _webViewManager;
public bool IsInitialized => _hostWindow != null;
public event EventHandler<GtkHostWindow>? HostWindowCreated;
public void Initialize(string title, int width, int height)
{
if (_hostWindow == null)
{
_hostWindow = new GtkHostWindow(title, width, height);
_webViewManager = new GtkWebViewManager(_hostWindow);
HostWindowCreated?.Invoke(this, _hostWindow);
}
}
public GtkHostWindow GetOrCreateHostWindow(string title = "MAUI Application", int width = 800, int height = 600)
{
if (_hostWindow == null)
{
Initialize(title, width, height);
}
return _hostWindow!;
}
public void SetWindowIcon(string iconPath)
{
_hostWindow?.SetIcon(iconPath);
}
public void Shutdown()
{
_webViewManager?.Clear();
_webViewManager = null;
_hostWindow?.Dispose();
_hostWindow = null;
}
}

32
Services/GtkMenuItem.cs Normal file
View File

@@ -0,0 +1,32 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Represents a menu item for use with GtkContextMenuService.
/// </summary>
public class GtkMenuItem
{
public string Text { get; }
public Action? Action { get; }
public bool IsEnabled { get; }
public bool IsSeparator { get; }
public static GtkMenuItem Separator => new GtkMenuItem();
public GtkMenuItem(string text, Action? action, bool isEnabled = true)
{
Text = text;
Action = action;
IsEnabled = isEnabled;
IsSeparator = false;
}
private GtkMenuItem()
{
Text = "";
Action = null;
IsEnabled = false;
IsSeparator = true;
}
}

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Generates application icons from MAUI icon metadata.
/// Creates PNG icons suitable for use as window icons on Linux.
/// Note: SVG overlay support requires Svg.Skia package (optional).
/// </summary>
public static class MauiIconGenerator
{
private const int DefaultIconSize = 256;
public static string? GenerateIcon(string metaFilePath)
{
if (!File.Exists(metaFilePath))
{
Console.WriteLine("[MauiIconGenerator] Metadata file not found: " + metaFilePath);
return null;
}
try
{
string path = Path.GetDirectoryName(metaFilePath) ?? "";
var metadata = ParseMetadata(File.ReadAllText(metaFilePath));
string outputPath = Path.Combine(path, "appicon.png");
int size = metadata.TryGetValue("Size", out var sizeStr) && int.TryParse(sizeStr, out var sizeVal)
? sizeVal
: DefaultIconSize;
SKColor color = metadata.TryGetValue("Color", out var colorStr)
? ParseColor(colorStr)
: SKColors.Purple;
Console.WriteLine($"[MauiIconGenerator] Generating {size}x{size} icon");
Console.WriteLine($"[MauiIconGenerator] Color: {color}");
using var surface = SKSurface.Create(new SKImageInfo(size, size, SKColorType.Bgra8888, SKAlphaType.Premul));
var canvas = surface.Canvas;
// Draw background with rounded corners
canvas.Clear(SKColors.Transparent);
float cornerRadius = size * 0.2f;
using var paint = new SKPaint { Color = color, IsAntialias = true };
canvas.DrawRoundRect(new SKRoundRect(new SKRect(0, 0, size, size), cornerRadius), paint);
// Try to load PNG foreground as fallback (appicon_fg.png)
string fgPngPath = Path.Combine(path, "appicon_fg.png");
if (File.Exists(fgPngPath))
{
try
{
using var fgBitmap = SKBitmap.Decode(fgPngPath);
if (fgBitmap != null)
{
float scale = size * 0.65f / Math.Max(fgBitmap.Width, fgBitmap.Height);
float fgWidth = fgBitmap.Width * scale;
float fgHeight = fgBitmap.Height * scale;
float offsetX = (size - fgWidth) / 2f;
float offsetY = (size - fgHeight) / 2f;
var dstRect = new SKRect(offsetX, offsetY, offsetX + fgWidth, offsetY + fgHeight);
canvas.DrawBitmap(fgBitmap, dstRect);
}
}
catch (Exception ex)
{
Console.WriteLine("[MauiIconGenerator] Failed to load foreground PNG: " + ex.Message);
}
}
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var fileStream = File.OpenWrite(outputPath);
data.SaveTo(fileStream);
Console.WriteLine("[MauiIconGenerator] Generated: " + outputPath);
return outputPath;
}
catch (Exception ex)
{
Console.WriteLine("[MauiIconGenerator] Error: " + ex.Message);
return null;
}
}
private static Dictionary<string, string> ParseMetadata(string content)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var parts = line.Split('=', 2);
if (parts.Length == 2)
{
result[parts[0].Trim()] = parts[1].Trim();
}
}
return result;
}
private static SKColor ParseColor(string colorStr)
{
if (string.IsNullOrEmpty(colorStr))
{
return SKColors.Purple;
}
colorStr = colorStr.Trim();
if (colorStr.StartsWith("#"))
{
string hex = colorStr.Substring(1);
// Expand 3-digit hex to 6-digit
if (hex.Length == 3)
{
hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
}
if (hex.Length == 6 && uint.TryParse(hex, NumberStyles.HexNumber, null, out var rgb))
{
return new SKColor(
(byte)((rgb >> 16) & 0xFF),
(byte)((rgb >> 8) & 0xFF),
(byte)(rgb & 0xFF));
}
if (hex.Length == 8 && uint.TryParse(hex, NumberStyles.HexNumber, null, out var argb))
{
return new SKColor(
(byte)((argb >> 16) & 0xFF),
(byte)((argb >> 8) & 0xFF),
(byte)(argb & 0xFF),
(byte)((argb >> 24) & 0xFF));
}
}
return colorStr.ToLowerInvariant() switch
{
"red" => SKColors.Red,
"green" => SKColors.Green,
"blue" => SKColors.Blue,
"purple" => SKColors.Purple,
"orange" => SKColors.Orange,
"white" => SKColors.White,
"black" => SKColors.Black,
_ => SKColors.Purple,
};
}
}

101
Views/LinuxDialogService.cs Normal file
View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
public static class LinuxDialogService
{
private static readonly List<SkiaAlertDialog> _activeDialogs = new List<SkiaAlertDialog>();
private static Action? _invalidateCallback;
private static SkiaContextMenu? _activeContextMenu;
private static Action? _showPopupCallback;
private static Action? _hidePopupCallback;
public static bool HasActiveDialog => _activeDialogs.Count > 0;
public static SkiaAlertDialog? TopDialog
{
get
{
if (_activeDialogs.Count <= 0)
{
return null;
}
return _activeDialogs[_activeDialogs.Count - 1];
}
}
public static SkiaContextMenu? ActiveContextMenu => _activeContextMenu;
public static bool HasContextMenu => _activeContextMenu != null;
public static void SetInvalidateCallback(Action callback)
{
_invalidateCallback = callback;
}
public static Task<bool> ShowAlertAsync(string title, string message, string? accept, string? cancel)
{
var dialog = new SkiaAlertDialog(title, message, accept, cancel);
_activeDialogs.Add(dialog);
_invalidateCallback?.Invoke();
return dialog.Result;
}
internal static void HideDialog(SkiaAlertDialog dialog)
{
_activeDialogs.Remove(dialog);
_invalidateCallback?.Invoke();
}
public static void DrawDialogs(SKCanvas canvas, SKRect bounds)
{
DrawDialogsOnly(canvas, bounds);
DrawContextMenuOnly(canvas, bounds);
}
public static void DrawDialogsOnly(SKCanvas canvas, SKRect bounds)
{
foreach (var dialog in _activeDialogs)
{
dialog.Measure(new SKSize(bounds.Width, bounds.Height));
dialog.Arrange(bounds);
dialog.Draw(canvas);
}
}
public static void DrawContextMenuOnly(SKCanvas canvas, SKRect bounds)
{
if (_activeContextMenu != null)
{
_activeContextMenu.Draw(canvas);
}
}
public static void SetPopupCallbacks(Action showPopup, Action hidePopup)
{
_showPopupCallback = showPopup;
_hidePopupCallback = hidePopup;
}
public static void ShowContextMenu(SkiaContextMenu menu)
{
Console.WriteLine("[LinuxDialogService] ShowContextMenu called");
_activeContextMenu = menu;
_showPopupCallback?.Invoke();
_invalidateCallback?.Invoke();
}
public static void HideContextMenu()
{
_activeContextMenu = null;
_hidePopupCallback?.Invoke();
_invalidateCallback?.Invoke();
}
}

View File

@@ -323,63 +323,3 @@ public class SkiaAlertDialog : SkiaView
return this; return this;
} }
} }
/// <summary>
/// Service for showing modal dialogs in OpenMaui Linux.
/// </summary>
public static class LinuxDialogService
{
private static readonly List<SkiaAlertDialog> _activeDialogs = new();
private static Action? _invalidateCallback;
/// <summary>
/// Registers the invalidation callback (called by LinuxApplication).
/// </summary>
public static void SetInvalidateCallback(Action callback)
{
_invalidateCallback = callback;
}
/// <summary>
/// Shows an alert dialog and returns when dismissed.
/// </summary>
public static Task<bool> ShowAlertAsync(string title, string message, string? accept, string? cancel)
{
var dialog = new SkiaAlertDialog(title, message, accept, cancel);
_activeDialogs.Add(dialog);
_invalidateCallback?.Invoke();
return dialog.Result;
}
/// <summary>
/// Hides a dialog.
/// </summary>
internal static void HideDialog(SkiaAlertDialog dialog)
{
_activeDialogs.Remove(dialog);
_invalidateCallback?.Invoke();
}
/// <summary>
/// Gets whether there are active dialogs.
/// </summary>
public static bool HasActiveDialog => _activeDialogs.Count > 0;
/// <summary>
/// Gets the topmost dialog.
/// </summary>
public static SkiaAlertDialog? TopDialog => _activeDialogs.Count > 0 ? _activeDialogs[^1] : null;
/// <summary>
/// Draws all active dialogs.
/// </summary>
public static void DrawDialogs(SKCanvas canvas, SKRect bounds)
{
foreach (var dialog in _activeDialogs)
{
dialog.Measure(new SKSize(bounds.Width, bounds.Height));
dialog.Arrange(bounds);
dialog.Draw(canvas);
}
}
}

View File

@@ -199,6 +199,39 @@ public class SkiaButton : SkiaView
typeof(SkiaButton), typeof(SkiaButton),
null); null);
/// <summary>
/// Bindable property for ImageSource.
/// </summary>
public static readonly BindableProperty ImageSourceProperty =
BindableProperty.Create(
nameof(ImageSource),
typeof(SKBitmap),
typeof(SkiaButton),
null,
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
/// <summary>
/// Bindable property for ImageSpacing.
/// </summary>
public static readonly BindableProperty ImageSpacingProperty =
BindableProperty.Create(
nameof(ImageSpacing),
typeof(float),
typeof(SkiaButton),
8f,
propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure());
/// <summary>
/// Bindable property for ContentLayoutPosition (0=Left, 1=Top, 2=Right, 3=Bottom).
/// </summary>
public static readonly BindableProperty ContentLayoutPositionProperty =
BindableProperty.Create(
nameof(ContentLayoutPosition),
typeof(int),
typeof(SkiaButton),
0,
propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure());
#endregion #endregion
#region Properties #region Properties
@@ -356,6 +389,33 @@ public class SkiaButton : SkiaView
set => SetValue(CommandParameterProperty, value); set => SetValue(CommandParameterProperty, value);
} }
/// <summary>
/// Gets or sets the image source for the button.
/// </summary>
public SKBitmap? ImageSource
{
get => (SKBitmap?)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
/// <summary>
/// Gets or sets the spacing between the image and text.
/// </summary>
public float ImageSpacing
{
get => (float)GetValue(ImageSpacingProperty);
set => SetValue(ImageSpacingProperty, value);
}
/// <summary>
/// Gets or sets the content layout position (0=Left, 1=Top, 2=Right, 3=Bottom).
/// </summary>
public int ContentLayoutPosition
{
get => (int)GetValue(ContentLayoutPositionProperty);
set => SetValue(ContentLayoutPositionProperty, value);
}
/// <summary> /// <summary>
/// Gets whether the button is currently pressed. /// Gets whether the button is currently pressed.
/// </summary> /// </summary>
@@ -504,8 +564,11 @@ public class SkiaButton : SkiaView
canvas.DrawRoundRect(focusRect, focusPaint); canvas.DrawRoundRect(focusRect, focusPaint);
} }
// Draw text // Draw content (text and/or image)
if (!string.IsNullOrEmpty(Text)) DrawContent(canvas, bounds, isTextOnly);
}
private void DrawContent(SKCanvas canvas, SKRect bounds, bool isTextOnly)
{ {
var fontStyle = new SKFontStyle( var fontStyle = new SKFontStyle(
IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
@@ -516,7 +579,7 @@ public class SkiaButton : SkiaView
using var font = new SKFont(typeface, FontSize); using var font = new SKFont(typeface, FontSize);
// For text-only buttons, darken text on hover/press for feedback // Determine text color
SKColor textColorToUse; SKColor textColorToUse;
if (!IsEnabled) if (!IsEnabled)
{ {
@@ -524,7 +587,6 @@ public class SkiaButton : SkiaView
} }
else if (isTextOnly && (IsHovered || IsPressed)) else if (isTextOnly && (IsHovered || IsPressed))
{ {
// Darken the text color slightly for hover/press feedback
textColorToUse = new SKColor( textColorToUse = new SKColor(
(byte)Math.Max(0, TextColor.Red - 40), (byte)Math.Max(0, TextColor.Red - 40),
(byte)Math.Max(0, TextColor.Green - 40), (byte)Math.Max(0, TextColor.Green - 40),
@@ -536,7 +598,7 @@ public class SkiaButton : SkiaView
textColorToUse = TextColor; textColorToUse = TextColor;
} }
using var paint = new SKPaint(font) using var textPaint = new SKPaint(font)
{ {
Color = textColorToUse, Color = textColorToUse,
IsAntialias = true IsAntialias = true
@@ -544,13 +606,109 @@ public class SkiaButton : SkiaView
// Measure text // Measure text
var textBounds = new SKRect(); var textBounds = new SKRect();
paint.MeasureText(Text, ref textBounds); bool hasText = !string.IsNullOrEmpty(Text);
if (hasText)
{
textPaint.MeasureText(Text, ref textBounds);
}
// Center text // Calculate image size
bool hasImage = ImageSource != null;
float imageWidth = 0;
float imageHeight = 0;
if (hasImage)
{
float maxImageSize = Math.Min(bounds.Height - 8, 24f);
float scale = Math.Min(maxImageSize / ImageSource!.Width, maxImageSize / ImageSource.Height);
imageWidth = ImageSource.Width * scale;
imageHeight = ImageSource.Height * scale;
}
// Calculate total content size and position
bool isHorizontal = ContentLayoutPosition == 0 || ContentLayoutPosition == 2;
float totalWidth, totalHeight;
if (hasImage && hasText)
{
if (isHorizontal)
{
totalWidth = imageWidth + ImageSpacing + textBounds.Width;
totalHeight = Math.Max(imageHeight, textBounds.Height);
}
else
{
totalWidth = Math.Max(imageWidth, textBounds.Width);
totalHeight = imageHeight + ImageSpacing + textBounds.Height;
}
}
else if (hasImage)
{
totalWidth = imageWidth;
totalHeight = imageHeight;
}
else
{
totalWidth = textBounds.Width;
totalHeight = textBounds.Height;
}
float startX = bounds.MidX - totalWidth / 2;
float startY = bounds.MidY - totalHeight / 2;
// Draw image and text based on layout position
if (hasImage)
{
float imageX, imageY;
float textX = 0, textY = 0;
switch (ContentLayoutPosition)
{
case 1: // Top - image above text
imageX = bounds.MidX - imageWidth / 2;
imageY = startY;
textX = bounds.MidX - textBounds.Width / 2;
textY = startY + imageHeight + ImageSpacing - textBounds.Top;
break;
case 2: // Right - image to right of text
textX = startX;
textY = bounds.MidY - textBounds.MidY;
imageX = startX + textBounds.Width + ImageSpacing;
imageY = bounds.MidY - imageHeight / 2;
break;
case 3: // Bottom - image below text
textX = bounds.MidX - textBounds.Width / 2;
textY = startY - textBounds.Top;
imageX = bounds.MidX - imageWidth / 2;
imageY = startY + textBounds.Height + ImageSpacing;
break;
default: // 0 = Left - image to left of text
imageX = startX;
imageY = bounds.MidY - imageHeight / 2;
textX = startX + imageWidth + ImageSpacing;
textY = bounds.MidY - textBounds.MidY;
break;
}
// Draw image
var imageRect = new SKRect(imageX, imageY, imageX + imageWidth, imageY + imageHeight);
using var imagePaint = new SKPaint { IsAntialias = true };
if (!IsEnabled)
{
imagePaint.ColorFilter = SKColorFilter.CreateBlendMode(new SKColor(128, 128, 128, 128), SKBlendMode.Modulate);
}
canvas.DrawBitmap(ImageSource!, imageRect, imagePaint);
// Draw text
if (hasText)
{
canvas.DrawText(Text!, textX, textY, textPaint);
}
}
else if (hasText)
{
// Just text, centered
var x = bounds.MidX - textBounds.MidX; var x = bounds.MidX - textBounds.MidX;
var y = bounds.MidY - textBounds.MidY; var y = bounds.MidY - textBounds.MidY;
canvas.DrawText(Text!, x, y, textPaint);
canvas.DrawText(Text, x, y, paint);
} }
} }

221
Views/SkiaContextMenu.cs Normal file
View File

@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
public class SkiaContextMenu : SkiaView
{
private readonly List<ContextMenuItem> _items;
private readonly float _x;
private readonly float _y;
private int _hoveredIndex = -1;
private SKRect[] _itemBounds = Array.Empty<SKRect>();
private static readonly SKColor MenuBackground = new SKColor(255, 255, 255);
private static readonly SKColor MenuBackgroundDark = new SKColor(48, 48, 48);
private static readonly SKColor ItemHoverBackground = new SKColor(227, 242, 253);
private static readonly SKColor ItemHoverBackgroundDark = new SKColor(80, 80, 80);
private static readonly SKColor ItemTextColor = new SKColor(33, 33, 33);
private static readonly SKColor ItemTextColorDark = new SKColor(224, 224, 224);
private static readonly SKColor DisabledTextColor = new SKColor(158, 158, 158);
private static readonly SKColor SeparatorColor = new SKColor(224, 224, 224);
private static readonly SKColor ShadowColor = new SKColor(0, 0, 0, 40);
private const float MenuPadding = 4f;
private const float ItemHeight = 32f;
private const float ItemPaddingH = 16f;
private const float SeparatorHeight = 9f;
private const float CornerRadius = 4f;
private const float MinWidth = 120f;
private bool _isDarkTheme;
public SkiaContextMenu(float x, float y, List<ContextMenuItem> items, bool isDarkTheme = false)
{
_x = x;
_y = y;
_items = items;
_isDarkTheme = isDarkTheme;
IsFocusable = true;
}
public override void Draw(SKCanvas canvas)
{
float menuWidth = CalculateMenuWidth();
float menuHeight = CalculateMenuHeight();
float posX = _x;
float posY = _y;
// Adjust position to stay within bounds
canvas.GetDeviceClipBounds(out var clipBounds);
if (posX + menuWidth > clipBounds.Right)
{
posX = clipBounds.Right - menuWidth - 4f;
}
if (posY + menuHeight > clipBounds.Bottom)
{
posY = clipBounds.Bottom - menuHeight - 4f;
}
var menuRect = new SKRect(posX, posY, posX + menuWidth, posY + menuHeight);
// Draw shadow
using (var shadowPaint = new SKPaint
{
Color = ShadowColor,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4f)
})
{
canvas.DrawRoundRect(menuRect.Left + 2f, menuRect.Top + 2f, menuWidth, menuHeight, CornerRadius, CornerRadius, shadowPaint);
}
// Draw background
using (var bgPaint = new SKPaint
{
Color = _isDarkTheme ? MenuBackgroundDark : MenuBackground,
IsAntialias = true
})
{
canvas.DrawRoundRect(menuRect, CornerRadius, CornerRadius, bgPaint);
}
// Draw border
using (var borderPaint = new SKPaint
{
Color = SeparatorColor,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1f,
IsAntialias = true
})
{
canvas.DrawRoundRect(menuRect, CornerRadius, CornerRadius, borderPaint);
}
// Draw items
_itemBounds = new SKRect[_items.Count];
float itemY = posY + MenuPadding;
for (int i = 0; i < _items.Count; i++)
{
var item = _items[i];
if (item.IsSeparator)
{
float separatorY = itemY + SeparatorHeight / 2f;
using (var sepPaint = new SKPaint { Color = SeparatorColor, StrokeWidth = 1f })
{
canvas.DrawLine(posX + 8f, separatorY, posX + menuWidth - 8f, separatorY, sepPaint);
}
_itemBounds[i] = new SKRect(posX, itemY, posX + menuWidth, itemY + SeparatorHeight);
itemY += SeparatorHeight;
continue;
}
var itemRect = new SKRect(posX + MenuPadding, itemY, posX + menuWidth - MenuPadding, itemY + ItemHeight);
_itemBounds[i] = itemRect;
// Draw hover background
if (i == _hoveredIndex && item.IsEnabled)
{
using (var hoverPaint = new SKPaint
{
Color = _isDarkTheme ? ItemHoverBackgroundDark : ItemHoverBackground,
IsAntialias = true
})
{
canvas.DrawRoundRect(itemRect, CornerRadius, CornerRadius, hoverPaint);
}
}
// Draw text
using (var textPaint = new SKPaint
{
Color = !item.IsEnabled ? DisabledTextColor : (_isDarkTheme ? ItemTextColorDark : ItemTextColor),
TextSize = 14f,
IsAntialias = true,
Typeface = SKTypeface.Default
})
{
float textY = itemRect.MidY + textPaint.TextSize / 3f;
canvas.DrawText(item.Text, itemRect.Left + ItemPaddingH, textY, textPaint);
}
itemY += ItemHeight;
}
}
private float CalculateMenuWidth()
{
float maxWidth = MinWidth;
using (var paint = new SKPaint { TextSize = 14f, Typeface = SKTypeface.Default })
{
foreach (var item in _items)
{
if (!item.IsSeparator)
{
float textWidth = paint.MeasureText(item.Text) + ItemPaddingH * 2f;
maxWidth = Math.Max(maxWidth, textWidth);
}
}
}
return maxWidth + MenuPadding * 2f;
}
private float CalculateMenuHeight()
{
float height = MenuPadding * 2f;
foreach (var item in _items)
{
height += item.IsSeparator ? SeparatorHeight : ItemHeight;
}
return height;
}
public override void OnPointerMoved(PointerEventArgs e)
{
int oldHovered = _hoveredIndex;
_hoveredIndex = -1;
for (int i = 0; i < _itemBounds.Length; i++)
{
if (_itemBounds[i].Contains(e.X, e.Y) && !_items[i].IsSeparator)
{
_hoveredIndex = i;
break;
}
}
if (oldHovered != _hoveredIndex)
{
Invalidate();
}
}
public override void OnPointerPressed(PointerEventArgs e)
{
for (int i = 0; i < _itemBounds.Length; i++)
{
if (_itemBounds[i].Contains(e.X, e.Y))
{
var item = _items[i];
if (item.IsEnabled && !item.IsSeparator && item.Action != null)
{
LinuxDialogService.HideContextMenu();
item.Action();
return;
}
}
}
LinuxDialogService.HideContextMenu();
}
public override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
LinuxDialogService.HideContextMenu();
e.Handled = true;
}
}
}

View File

@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using SkiaSharp; using SkiaSharp;
using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -980,11 +982,17 @@ public class SkiaEntry : SkiaView
public override void OnPointerPressed(PointerEventArgs e) public override void OnPointerPressed(PointerEventArgs e)
{ {
Console.WriteLine($"[SkiaEntry] OnPointerPressed - Text='{Text}', Placeholder='{Placeholder}', IsEnabled={IsEnabled}, IsFocused={IsFocused}"); Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})");
Console.WriteLine($"[SkiaEntry] Bounds={Bounds}, ScreenBounds={ScreenBounds}, e.X={e.X}, e.Y={e.Y}");
if (!IsEnabled) return; if (!IsEnabled) return;
// Handle right-click context menu
if (e.Button == PointerButton.Right)
{
Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu");
ShowContextMenu(e.X, e.Y);
return;
}
// Check if clicked on clear button // Check if clicked on clear button
if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused)
{ {
@@ -1217,6 +1225,38 @@ public class SkiaEntry : SkiaView
Invalidate(); Invalidate();
} }
private void ShowContextMenu(float x, float y)
{
Console.WriteLine($"[SkiaEntry] ShowContextMenu at ({x}, {y})");
bool hasSelection = _selectionLength != 0;
bool hasText = !string.IsNullOrEmpty(Text);
bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText());
GtkContextMenuService.ShowContextMenu(new List<GtkMenuItem>
{
new GtkMenuItem("Cut", () =>
{
CutToClipboard();
Invalidate();
}, hasSelection),
new GtkMenuItem("Copy", () =>
{
CopyToClipboard();
}, hasSelection),
new GtkMenuItem("Paste", () =>
{
PasteFromClipboard();
Invalidate();
}, hasClipboard),
GtkMenuItem.Separator,
new GtkMenuItem("Select All", () =>
{
SelectAll();
Invalidate();
}, hasText)
});
}
public override void OnFocusGained() public override void OnFocusGained()
{ {
base.OnFocusGained(); base.OnFocusGained();

256
Views/SkiaFlexLayout.cs Normal file
View File

@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Maui.Controls;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
public class SkiaFlexLayout : SkiaLayoutView
{
public static readonly BindableProperty DirectionProperty = BindableProperty.Create(
nameof(Direction), typeof(FlexDirection), typeof(SkiaFlexLayout), FlexDirection.Row,
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
public static readonly BindableProperty WrapProperty = BindableProperty.Create(
nameof(Wrap), typeof(FlexWrap), typeof(SkiaFlexLayout), FlexWrap.NoWrap,
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
public static readonly BindableProperty JustifyContentProperty = BindableProperty.Create(
nameof(JustifyContent), typeof(FlexJustify), typeof(SkiaFlexLayout), FlexJustify.Start,
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
public static readonly BindableProperty AlignItemsProperty = BindableProperty.Create(
nameof(AlignItems), typeof(FlexAlignItems), typeof(SkiaFlexLayout), FlexAlignItems.Stretch,
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
public static readonly BindableProperty AlignContentProperty = BindableProperty.Create(
nameof(AlignContent), typeof(FlexAlignContent), typeof(SkiaFlexLayout), FlexAlignContent.Stretch,
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
public static readonly BindableProperty OrderProperty = BindableProperty.CreateAttached(
"Order", typeof(int), typeof(SkiaFlexLayout), 0, BindingMode.TwoWay);
public static readonly BindableProperty GrowProperty = BindableProperty.CreateAttached(
"Grow", typeof(float), typeof(SkiaFlexLayout), 0f, BindingMode.TwoWay);
public static readonly BindableProperty ShrinkProperty = BindableProperty.CreateAttached(
"Shrink", typeof(float), typeof(SkiaFlexLayout), 1f, BindingMode.TwoWay);
public static readonly BindableProperty BasisProperty = BindableProperty.CreateAttached(
"Basis", typeof(FlexBasis), typeof(SkiaFlexLayout), FlexBasis.Auto, BindingMode.TwoWay);
public static readonly BindableProperty AlignSelfProperty = BindableProperty.CreateAttached(
"AlignSelf", typeof(FlexAlignSelf), typeof(SkiaFlexLayout), FlexAlignSelf.Auto, BindingMode.TwoWay);
public FlexDirection Direction
{
get => (FlexDirection)GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public FlexWrap Wrap
{
get => (FlexWrap)GetValue(WrapProperty);
set => SetValue(WrapProperty, value);
}
public FlexJustify JustifyContent
{
get => (FlexJustify)GetValue(JustifyContentProperty);
set => SetValue(JustifyContentProperty, value);
}
public FlexAlignItems AlignItems
{
get => (FlexAlignItems)GetValue(AlignItemsProperty);
set => SetValue(AlignItemsProperty, value);
}
public FlexAlignContent AlignContent
{
get => (FlexAlignContent)GetValue(AlignContentProperty);
set => SetValue(AlignContentProperty, value);
}
public static int GetOrder(SkiaView view) => (int)view.GetValue(OrderProperty);
public static void SetOrder(SkiaView view, int value) => view.SetValue(OrderProperty, value);
public static float GetGrow(SkiaView view) => (float)view.GetValue(GrowProperty);
public static void SetGrow(SkiaView view, float value) => view.SetValue(GrowProperty, value);
public static float GetShrink(SkiaView view) => (float)view.GetValue(ShrinkProperty);
public static void SetShrink(SkiaView view, float value) => view.SetValue(ShrinkProperty, value);
public static FlexBasis GetBasis(SkiaView view) => (FlexBasis)view.GetValue(BasisProperty);
public static void SetBasis(SkiaView view, FlexBasis value) => view.SetValue(BasisProperty, value);
public static FlexAlignSelf GetAlignSelf(SkiaView view) => (FlexAlignSelf)view.GetValue(AlignSelfProperty);
public static void SetAlignSelf(SkiaView view, FlexAlignSelf value) => view.SetValue(AlignSelfProperty, value);
protected override SKSize MeasureOverride(SKSize availableSize)
{
bool isRow = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse;
float totalMain = 0f;
float maxCross = 0f;
foreach (var child in Children)
{
if (!child.IsVisible)
continue;
var childSize = child.Measure(availableSize);
if (isRow)
{
totalMain += childSize.Width;
maxCross = Math.Max(maxCross, childSize.Height);
}
else
{
totalMain += childSize.Height;
maxCross = Math.Max(maxCross, childSize.Width);
}
}
return isRow ? new SKSize(totalMain, maxCross) : new SKSize(maxCross, totalMain);
}
protected override SKRect ArrangeOverride(SKRect bounds)
{
if (Children.Count == 0)
return bounds;
bool isRow = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse;
bool isReverse = Direction == FlexDirection.RowReverse || Direction == FlexDirection.ColumnReverse;
var orderedChildren = Children.Where(c => c.IsVisible).OrderBy(c => GetOrder(c)).ToList();
if (orderedChildren.Count == 0)
return bounds;
float mainSize = isRow ? bounds.Width : bounds.Height;
float crossSize = isRow ? bounds.Height : bounds.Width;
var childInfos = new List<(SkiaView child, SKSize size, float grow, float shrink)>();
float totalBasis = 0f;
float totalGrow = 0f;
float totalShrink = 0f;
foreach (var child in orderedChildren)
{
var basis = GetBasis(child);
float grow = GetGrow(child);
float shrink = GetShrink(child);
SKSize size;
if (basis.IsAuto)
{
size = child.Measure(new SKSize(bounds.Width, bounds.Height));
}
else
{
float length = basis.Length;
size = isRow
? child.Measure(new SKSize(length, bounds.Height))
: child.Measure(new SKSize(bounds.Width, length));
}
childInfos.Add((child, size, grow, shrink));
totalBasis += isRow ? size.Width : size.Height;
totalGrow += grow;
totalShrink += shrink;
}
float freeSpace = mainSize - totalBasis;
var resolvedSizes = new List<(SkiaView child, float mainSize, float crossSize)>();
foreach (var (child, size, grow, shrink) in childInfos)
{
float childMainSize = isRow ? size.Width : size.Height;
float childCrossSize = isRow ? size.Height : size.Width;
if (freeSpace > 0f && totalGrow > 0f)
{
childMainSize += freeSpace * (grow / totalGrow);
}
else if (freeSpace < 0f && totalShrink > 0f)
{
childMainSize += freeSpace * (shrink / totalShrink);
}
resolvedSizes.Add((child, Math.Max(0f, childMainSize), childCrossSize));
}
float usedSpace = resolvedSizes.Sum(s => s.mainSize);
float remainingSpace = Math.Max(0f, mainSize - usedSpace);
float position = isRow ? bounds.Left : bounds.Top;
float spacing = 0f;
switch (JustifyContent)
{
case FlexJustify.Center:
position += remainingSpace / 2f;
break;
case FlexJustify.End:
position += remainingSpace;
break;
case FlexJustify.SpaceBetween:
if (resolvedSizes.Count > 1)
spacing = remainingSpace / (resolvedSizes.Count - 1);
break;
case FlexJustify.SpaceAround:
if (resolvedSizes.Count > 0)
{
spacing = remainingSpace / resolvedSizes.Count;
position += spacing / 2f;
}
break;
case FlexJustify.SpaceEvenly:
if (resolvedSizes.Count > 0)
{
spacing = remainingSpace / (resolvedSizes.Count + 1);
position += spacing;
}
break;
}
var items = isReverse ? resolvedSizes.AsEnumerable().Reverse() : resolvedSizes;
foreach (var (child, childMainSize, childCrossSize) in items)
{
var alignSelf = GetAlignSelf(child);
var effectiveAlign = alignSelf == FlexAlignSelf.Auto ? AlignItems : (FlexAlignItems)alignSelf;
float crossPos = isRow ? bounds.Top : bounds.Left;
float finalCrossSize = childCrossSize;
switch (effectiveAlign)
{
case FlexAlignItems.End:
crossPos = (isRow ? bounds.Bottom : bounds.Right) - finalCrossSize;
break;
case FlexAlignItems.Center:
crossPos += (crossSize - finalCrossSize) / 2f;
break;
case FlexAlignItems.Stretch:
finalCrossSize = crossSize;
break;
}
SKRect childBounds;
if (isRow)
{
childBounds = new SKRect(position, crossPos, position + childMainSize, crossPos + finalCrossSize);
}
else
{
childBounds = new SKRect(crossPos, position, crossPos + finalCrossSize, position + childMainSize);
}
child.Arrange(childBounds);
position += childMainSize + spacing;
}
return bounds;
}
}

View File

@@ -210,6 +210,25 @@ public class SkiaImage : SkiaView
} }
} }
/// <summary>
/// Loads the image from an SKBitmap.
/// </summary>
public void LoadFromBitmap(SKBitmap bitmap)
{
try
{
Bitmap = bitmap;
_isLoading = false;
ImageLoaded?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
_isLoading = false;
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
}
Invalidate();
}
protected override SKSize MeasureOverride(SKSize availableSize) protected override SKSize MeasureOverride(SKSize availableSize)
{ {
if (_image == null) if (_image == null)

View File

@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using SkiaSharp; using SkiaSharp;
using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Rendering;
@@ -24,6 +27,17 @@ public class SkiaLabel : SkiaView
"", "",
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
/// <summary>
/// Bindable property for FormattedSpans.
/// </summary>
public static readonly BindableProperty FormattedSpansProperty =
BindableProperty.Create(
nameof(FormattedSpans),
typeof(IList<SkiaTextSpan>),
typeof(SkiaLabel),
null,
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
/// <summary> /// <summary>
/// Bindable property for TextColor. /// Bindable property for TextColor.
/// </summary> /// </summary>
@@ -191,6 +205,15 @@ public class SkiaLabel : SkiaView
set => SetValue(TextProperty, value); set => SetValue(TextProperty, value);
} }
/// <summary>
/// Gets or sets the formatted text spans for rich text rendering.
/// </summary>
public IList<SkiaTextSpan>? FormattedSpans
{
get => (IList<SkiaTextSpan>?)GetValue(FormattedSpansProperty);
set => SetValue(FormattedSpansProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the text color. /// Gets or sets the text color.
/// </summary> /// </summary>
@@ -363,6 +386,11 @@ public class SkiaLabel : SkiaView
private static SKTypeface? _cachedTypeface; private static SKTypeface? _cachedTypeface;
/// <summary>
/// Event raised when the label is tapped.
/// </summary>
public event EventHandler? Tapped;
private void OnTextChanged() private void OnTextChanged()
{ {
InvalidateMeasure(); InvalidateMeasure();
@@ -400,6 +428,20 @@ public class SkiaLabel : SkiaView
protected override void OnDraw(SKCanvas canvas, SKRect bounds) protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{ {
// Calculate content bounds with padding
var contentBounds = new SKRect(
bounds.Left + Padding.Left,
bounds.Top + Padding.Top,
bounds.Right - Padding.Right,
bounds.Bottom - Padding.Bottom);
// Handle formatted spans first (rich text)
if (FormattedSpans != null && FormattedSpans.Count > 0)
{
DrawFormattedText(canvas, contentBounds);
return;
}
if (string.IsNullOrEmpty(Text)) if (string.IsNullOrEmpty(Text))
return; return;
@@ -421,13 +463,6 @@ public class SkiaLabel : SkiaView
IsAntialias = true IsAntialias = true
}; };
// Calculate content bounds with padding
var contentBounds = new SKRect(
bounds.Left + Padding.Left,
bounds.Top + Padding.Top,
bounds.Right - Padding.Right,
bounds.Bottom - Padding.Bottom);
// Handle single line vs multiline // Handle single line vs multiline
// Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled // Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') ||
@@ -815,6 +850,181 @@ public class SkiaLabel : SkiaView
totalHeight + Padding.Top + Padding.Bottom); totalHeight + Padding.Top + Padding.Bottom);
} }
} }
private void DrawFormattedText(SKCanvas canvas, SKRect bounds)
{
if (FormattedSpans == null || FormattedSpans.Count == 0)
return;
float currentX = bounds.Left;
float currentY = bounds.Top;
float lineHeight = 0f;
// First pass: calculate line data
var lineSpans = new List<(SkiaTextSpan span, float x, float width, float height, SKPaint paint)>();
foreach (var span in FormattedSpans)
{
if (string.IsNullOrEmpty(span.Text))
continue;
var paint = CreateSpanPaint(span);
var textBounds = new SKRect();
paint.MeasureText(span.Text, ref textBounds);
lineHeight = Math.Max(lineHeight, textBounds.Height);
// Word wrap
if (currentX + textBounds.Width > bounds.Right && currentX > bounds.Left)
{
currentY += lineHeight * LineHeight;
currentX = bounds.Left;
lineHeight = textBounds.Height;
}
lineSpans.Add((span, currentX, textBounds.Width, textBounds.Height, paint));
currentX += textBounds.Width;
}
// Calculate vertical offset
float totalHeight = currentY + lineHeight - bounds.Top;
float verticalOffset = VerticalTextAlignment switch
{
TextAlignment.Start => 0f,
TextAlignment.Center => (bounds.Height - totalHeight) / 2f,
TextAlignment.End => bounds.Height - totalHeight,
_ => 0f
};
// Second pass: draw with alignment
currentX = bounds.Left;
currentY = bounds.Top + verticalOffset;
lineHeight = 0f;
var currentLine = new List<(SkiaTextSpan span, float relX, float width, float height, SKPaint paint)>();
float lineLeft = bounds.Left;
foreach (var span in FormattedSpans)
{
if (string.IsNullOrEmpty(span.Text))
continue;
var paint = CreateSpanPaint(span);
var textBounds = new SKRect();
paint.MeasureText(span.Text, ref textBounds);
lineHeight = Math.Max(lineHeight, textBounds.Height);
if (currentX + textBounds.Width > bounds.Right && currentX > bounds.Left)
{
DrawFormattedLine(canvas, bounds, currentLine, currentY + lineHeight);
currentY += lineHeight * LineHeight;
currentX = bounds.Left;
lineHeight = textBounds.Height;
currentLine.Clear();
}
currentLine.Add((span, currentX - lineLeft, textBounds.Width, textBounds.Height, paint));
currentX += textBounds.Width;
}
if (currentLine.Count > 0)
{
DrawFormattedLine(canvas, bounds, currentLine, currentY + lineHeight);
}
}
private void DrawFormattedLine(SKCanvas canvas, SKRect bounds,
List<(SkiaTextSpan span, float x, float width, float height, SKPaint paint)> lineSpans, float y)
{
if (lineSpans.Count == 0) return;
float lineWidth = lineSpans.Sum(s => s.width);
float startX = HorizontalTextAlignment switch
{
TextAlignment.Start => bounds.Left,
TextAlignment.Center => bounds.Left + (bounds.Width - lineWidth) / 2f,
TextAlignment.End => bounds.Right - lineWidth,
_ => bounds.Left
};
foreach (var (span, relX, width, height, paint) in lineSpans)
{
float x = startX + relX;
// Draw background if specified
if (span.BackgroundColor.HasValue && span.BackgroundColor.Value != SKColors.Transparent)
{
using var bgPaint = new SKPaint
{
Color = span.BackgroundColor.Value,
Style = SKPaintStyle.Fill
};
canvas.DrawRect(x, y - height, width, height + 4f, bgPaint);
}
canvas.DrawText(span.Text, x, y, paint);
// Draw underline
if (span.IsUnderline)
{
using var linePaint = new SKPaint
{
Color = paint.Color,
StrokeWidth = 1f,
IsAntialias = true
};
canvas.DrawLine(x, y + 2f, x + width, y + 2f, linePaint);
}
// Draw strikethrough
if (span.IsStrikethrough)
{
using var linePaint = new SKPaint
{
Color = paint.Color,
StrokeWidth = 1f,
IsAntialias = true
};
canvas.DrawLine(x, y - height / 3f, x + width, y - height / 3f, linePaint);
}
paint.Dispose();
}
}
private SKPaint CreateSpanPaint(SkiaTextSpan span)
{
var fontStyle = new SKFontStyle(
span.IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
SKFontStyleWidth.Normal,
span.IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(span.FontFamily ?? FontFamily, fontStyle);
if (typeface == null || typeface == SKTypeface.Default)
{
typeface = GetLinuxTypeface();
}
var fontSize = span.FontSize > 0f ? span.FontSize : FontSize;
using var font = new SKFont(typeface, fontSize);
var color = span.TextColor ?? TextColor;
if (!IsEnabled)
{
color = color.WithAlpha(128);
}
return new SKPaint(font)
{
Color = color,
IsAntialias = true
};
}
public override void OnPointerPressed(PointerEventArgs e)
{
base.OnPointerPressed(e);
Tapped?.Invoke(this, EventArgs.Empty);
}
} }
/// <summary> /// <summary>

View File

@@ -284,6 +284,21 @@ public class SkiaShell : SkiaLayoutView
/// </summary> /// </summary>
public int CurrentSectionIndex => _selectedSectionIndex; public int CurrentSectionIndex => _selectedSectionIndex;
/// <summary>
/// Reference to the MAUI Shell this view represents.
/// </summary>
public Shell? MauiShell { get; set; }
/// <summary>
/// Callback to render content from a ShellContent.
/// </summary>
public Func<Microsoft.Maui.Controls.ShellContent, SkiaView?>? ContentRenderer { get; set; }
/// <summary>
/// Callback to refresh shell colors.
/// </summary>
public Action<SkiaShell, Shell>? ColorRefresher { get; set; }
/// <summary> /// <summary>
/// Event raised when FlyoutIsPresented changes. /// Event raised when FlyoutIsPresented changes.
/// </summary> /// </summary>
@@ -342,6 +357,48 @@ public class SkiaShell : SkiaLayoutView
Invalidate(); Invalidate();
} }
/// <summary>
/// Refreshes the shell theme and re-renders all pages.
/// </summary>
public void RefreshTheme()
{
Console.WriteLine("[SkiaShell] RefreshTheme called - refreshing all pages");
if (MauiShell != null && ColorRefresher != null)
{
Console.WriteLine("[SkiaShell] Refreshing shell colors");
ColorRefresher(this, MauiShell);
}
if (ContentRenderer != null)
{
foreach (var section in _sections)
{
foreach (var item in section.Items)
{
if (item.MauiShellContent != null)
{
Console.WriteLine("[SkiaShell] Re-rendering: " + item.Title);
var skiaView = ContentRenderer(item.MauiShellContent);
if (skiaView != null)
{
item.Content = skiaView;
}
}
}
}
}
if (_selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count)
{
var section = _sections[_selectedSectionIndex];
if (_selectedItemIndex >= 0 && _selectedItemIndex < section.Items.Count)
{
var item = section.Items[_selectedItemIndex];
SetCurrentContent(item.Content);
}
}
InvalidateMeasure();
Invalidate();
}
/// <summary> /// <summary>
/// Navigates using a URI route. /// Navigates using a URI route.
/// </summary> /// </summary>
@@ -900,6 +957,11 @@ public class ShellContent
/// The content view. /// The content view.
/// </summary> /// </summary>
public SkiaView? Content { get; set; } public SkiaView? Content { get; set; }
/// <summary>
/// Reference to the MAUI ShellContent this represents.
/// </summary>
public Microsoft.Maui.Controls.ShellContent? MauiShellContent { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Window;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -218,6 +223,116 @@ public abstract class SkiaView : BindableObject, IDisposable
typeof(SkiaView), typeof(SkiaView),
string.Empty); string.Empty);
/// <summary>
/// Bindable property for Scale.
/// </summary>
public static readonly BindableProperty ScaleProperty =
BindableProperty.Create(
nameof(Scale),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for ScaleX.
/// </summary>
public static readonly BindableProperty ScaleXProperty =
BindableProperty.Create(
nameof(ScaleX),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for ScaleY.
/// </summary>
public static readonly BindableProperty ScaleYProperty =
BindableProperty.Create(
nameof(ScaleY),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for Rotation.
/// </summary>
public static readonly BindableProperty RotationProperty =
BindableProperty.Create(
nameof(Rotation),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for RotationX.
/// </summary>
public static readonly BindableProperty RotationXProperty =
BindableProperty.Create(
nameof(RotationX),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for RotationY.
/// </summary>
public static readonly BindableProperty RotationYProperty =
BindableProperty.Create(
nameof(RotationY),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for TranslationX.
/// </summary>
public static readonly BindableProperty TranslationXProperty =
BindableProperty.Create(
nameof(TranslationX),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for TranslationY.
/// </summary>
public static readonly BindableProperty TranslationYProperty =
BindableProperty.Create(
nameof(TranslationY),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for AnchorX.
/// </summary>
public static readonly BindableProperty AnchorXProperty =
BindableProperty.Create(
nameof(AnchorX),
typeof(double),
typeof(SkiaView),
0.5,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for AnchorY.
/// </summary>
public static readonly BindableProperty AnchorYProperty =
BindableProperty.Create(
nameof(AnchorY),
typeof(double),
typeof(SkiaView),
0.5,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
#endregion #endregion
private bool _disposed; private bool _disposed;
@@ -408,6 +523,107 @@ public abstract class SkiaView : BindableObject, IDisposable
set => SetValue(NameProperty, value); set => SetValue(NameProperty, value);
} }
/// <summary>
/// Gets or sets the uniform scale factor.
/// </summary>
public double Scale
{
get => (double)GetValue(ScaleProperty);
set => SetValue(ScaleProperty, value);
}
/// <summary>
/// Gets or sets the X-axis scale factor.
/// </summary>
public double ScaleX
{
get => (double)GetValue(ScaleXProperty);
set => SetValue(ScaleXProperty, value);
}
/// <summary>
/// Gets or sets the Y-axis scale factor.
/// </summary>
public double ScaleY
{
get => (double)GetValue(ScaleYProperty);
set => SetValue(ScaleYProperty, value);
}
/// <summary>
/// Gets or sets the rotation in degrees around the Z-axis.
/// </summary>
public double Rotation
{
get => (double)GetValue(RotationProperty);
set => SetValue(RotationProperty, value);
}
/// <summary>
/// Gets or sets the rotation in degrees around the X-axis.
/// </summary>
public double RotationX
{
get => (double)GetValue(RotationXProperty);
set => SetValue(RotationXProperty, value);
}
/// <summary>
/// Gets or sets the rotation in degrees around the Y-axis.
/// </summary>
public double RotationY
{
get => (double)GetValue(RotationYProperty);
set => SetValue(RotationYProperty, value);
}
/// <summary>
/// Gets or sets the X translation offset.
/// </summary>
public double TranslationX
{
get => (double)GetValue(TranslationXProperty);
set => SetValue(TranslationXProperty, value);
}
/// <summary>
/// Gets or sets the Y translation offset.
/// </summary>
public double TranslationY
{
get => (double)GetValue(TranslationYProperty);
set => SetValue(TranslationYProperty, value);
}
/// <summary>
/// Gets or sets the X anchor point for transforms (0.0 to 1.0).
/// </summary>
public double AnchorX
{
get => (double)GetValue(AnchorXProperty);
set => SetValue(AnchorXProperty, value);
}
/// <summary>
/// Gets or sets the Y anchor point for transforms (0.0 to 1.0).
/// </summary>
public double AnchorY
{
get => (double)GetValue(AnchorYProperty);
set => SetValue(AnchorYProperty, value);
}
/// <summary>
/// Gets or sets the cursor type when hovering over this view.
/// </summary>
public CursorType CursorType { get; set; }
/// <summary>
/// Gets or sets the MAUI View this platform view represents.
/// Used for gesture processing.
/// </summary>
public View? MauiView { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether this view currently has keyboard focus. /// Gets or sets whether this view currently has keyboard focus.
/// </summary> /// </summary>
@@ -566,8 +782,23 @@ public abstract class SkiaView : BindableObject, IDisposable
/// </summary> /// </summary>
public void Invalidate() public void Invalidate()
{ {
LinuxApplication.LogInvalidate(GetType().Name);
Invalidated?.Invoke(this, EventArgs.Empty); Invalidated?.Invoke(this, EventArgs.Empty);
_parent?.Invalidate();
// Notify rendering engine of dirty region
if (Bounds.Width > 0 && Bounds.Height > 0)
{
SkiaRenderingEngine.Current?.InvalidateRegion(Bounds);
}
if (_parent != null)
{
_parent.Invalidate();
}
else
{
LinuxApplication.RequestRedraw();
}
} }
/// <summary> /// <summary>
@@ -583,7 +814,7 @@ public abstract class SkiaView : BindableObject, IDisposable
/// <summary> /// <summary>
/// Draws this view and its children to the canvas. /// Draws this view and its children to the canvas.
/// </summary> /// </summary>
public void Draw(SKCanvas canvas) public virtual void Draw(SKCanvas canvas)
{ {
if (!IsVisible || Opacity <= 0) if (!IsVisible || Opacity <= 0)
{ {
@@ -592,6 +823,42 @@ public abstract class SkiaView : BindableObject, IDisposable
canvas.Save(); canvas.Save();
// Apply transforms if any are set
if (Scale != 1.0 || ScaleX != 1.0 || ScaleY != 1.0 ||
Rotation != 0.0 || RotationX != 0.0 || RotationY != 0.0 ||
TranslationX != 0.0 || TranslationY != 0.0)
{
// Calculate anchor point in absolute coordinates
float anchorAbsX = Bounds.Left + (float)(Bounds.Width * AnchorX);
float anchorAbsY = Bounds.Top + (float)(Bounds.Height * AnchorY);
// Move origin to anchor point
canvas.Translate(anchorAbsX, anchorAbsY);
// Apply translation
if (TranslationX != 0.0 || TranslationY != 0.0)
{
canvas.Translate((float)TranslationX, (float)TranslationY);
}
// Apply rotation
if (Rotation != 0.0)
{
canvas.RotateDegrees((float)Rotation);
}
// Apply scale
float scaleX = (float)(Scale * ScaleX);
float scaleY = (float)(Scale * ScaleY);
if (scaleX != 1f || scaleY != 1f)
{
canvas.Scale(scaleX, scaleY);
}
// Move origin back
canvas.Translate(-anchorAbsX, -anchorAbsY);
}
// Apply opacity // Apply opacity
if (Opacity < 1.0f) if (Opacity < 1.0f)
{ {
@@ -706,11 +973,47 @@ public abstract class SkiaView : BindableObject, IDisposable
#region Input Events #region Input Events
public virtual void OnPointerEntered(PointerEventArgs e) { } public virtual void OnPointerEntered(PointerEventArgs e)
public virtual void OnPointerExited(PointerEventArgs e) { } {
public virtual void OnPointerMoved(PointerEventArgs e) { } if (MauiView != null)
public virtual void OnPointerPressed(PointerEventArgs e) { } {
public virtual void OnPointerReleased(PointerEventArgs e) { } GestureManager.ProcessPointerEntered(MauiView, e.X, e.Y);
}
}
public virtual void OnPointerExited(PointerEventArgs e)
{
if (MauiView != null)
{
GestureManager.ProcessPointerExited(MauiView, e.X, e.Y);
}
}
public virtual void OnPointerMoved(PointerEventArgs e)
{
if (MauiView != null)
{
GestureManager.ProcessPointerMove(MauiView, e.X, e.Y);
}
}
public virtual void OnPointerPressed(PointerEventArgs e)
{
if (MauiView != null)
{
GestureManager.ProcessPointerDown(MauiView, e.X, e.Y);
}
}
public virtual void OnPointerReleased(PointerEventArgs e)
{
Console.WriteLine($"[SkiaView] OnPointerReleased on {GetType().Name}, MauiView={MauiView?.GetType().Name ?? "null"}");
if (MauiView != null)
{
GestureManager.ProcessPointerUp(MauiView, e.X, e.Y);
}
}
public virtual void OnScroll(ScrollEventArgs e) { } public virtual void OnScroll(ScrollEventArgs e) { }
public virtual void OnKeyDown(KeyEventArgs e) { } public virtual void OnKeyDown(KeyEventArgs e) { }
public virtual void OnKeyUp(KeyEventArgs e) { } public virtual void OnKeyUp(KeyEventArgs e) { }

View File

@@ -129,6 +129,37 @@ public class SkiaWebView : SkiaView
private const int RTLD_GLOBAL = 0x100; private const int RTLD_GLOBAL = 0x100;
private static IntPtr _webkitHandle; private static IntPtr _webkitHandle;
private static IntPtr _mainDisplay;
private static IntPtr _mainWindow;
private static readonly HashSet<SkiaWebView> _activeWebViews = new();
/// <summary>
/// Sets the main window for WebView operations.
/// </summary>
public static void SetMainWindow(IntPtr display, IntPtr window)
{
_mainDisplay = display;
_mainWindow = window;
Console.WriteLine($"[WebView] Main window set: display={display}, window={window}");
}
/// <summary>
/// Processes pending GTK events for WebViews.
/// </summary>
public static void ProcessGtkEvents()
{
bool hasActiveWebViews;
lock (_activeWebViews)
{
hasActiveWebViews = _activeWebViews.Count > 0;
}
if (hasActiveWebViews && _gtkInitialized)
{
while (g_main_context_iteration(IntPtr.Zero, mayBlock: false))
{
}
}
}
#endregion #endregion

11
Window/CursorType.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace Microsoft.Maui.Platform.Linux.Window;
/// <summary>
/// Types of cursors supported on Linux.
/// </summary>
public enum CursorType
{
Arrow,
Hand,
Text
}

343
Window/GtkHostWindow.cs Normal file
View File

@@ -0,0 +1,343 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Rendering;
namespace Microsoft.Maui.Platform.Linux.Window;
/// <summary>
/// GTK-based host window for MAUI applications on Linux.
/// Uses GTK3 with X11 backend for windowing and event handling.
/// </summary>
public sealed class GtkHostWindow : IDisposable
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool DeleteEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool ConfigureEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool ButtonEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool MotionEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData);
[StructLayout(LayoutKind.Explicit)]
private struct GdkEventButton
{
[FieldOffset(0)]
public int type;
[FieldOffset(8)]
public IntPtr window;
[FieldOffset(16)]
public sbyte send_event;
[FieldOffset(20)]
public uint time;
[FieldOffset(24)]
public double x;
[FieldOffset(32)]
public double y;
[FieldOffset(40)]
public IntPtr axes;
[FieldOffset(48)]
public uint state;
[FieldOffset(52)]
public uint button;
}
[StructLayout(LayoutKind.Explicit)]
private struct GdkEventMotion
{
[FieldOffset(0)]
public int type;
[FieldOffset(8)]
public IntPtr window;
[FieldOffset(16)]
public sbyte send_event;
[FieldOffset(20)]
public uint time;
[FieldOffset(24)]
public double x;
[FieldOffset(32)]
public double y;
}
private IntPtr _window;
private IntPtr _overlay;
private IntPtr _webViewLayer;
private GtkSkiaSurfaceWidget? _skiaSurface;
private bool _disposed;
private bool _isRunning;
private int _width;
private int _height;
private readonly DeleteEventDelegate _deleteEventHandler;
private readonly ConfigureEventDelegate _configureEventHandler;
private readonly ButtonEventDelegate _buttonPressHandler;
private readonly ButtonEventDelegate _buttonReleaseHandler;
private readonly MotionEventDelegate _motionHandler;
public IntPtr Window => _window;
public IntPtr Overlay => _overlay;
public IntPtr WebViewLayer => _webViewLayer;
public GtkSkiaSurfaceWidget? SkiaSurface => _skiaSurface;
public int Width => _width;
public int Height => _height;
public bool IsRunning => _isRunning;
public event EventHandler<(int Width, int Height)>? Resized;
public event EventHandler? CloseRequested;
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 GtkHostWindow(string title, int width, int height)
{
_width = width;
_height = height;
// Configure environment for GTK/X11
Environment.SetEnvironmentVariable("GDK_BACKEND", "x11");
Environment.SetEnvironmentVariable("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1");
Environment.SetEnvironmentVariable("LIBGL_ALWAYS_SOFTWARE", "1");
int argc = 0;
IntPtr argv = IntPtr.Zero;
if (!GtkNative.gtk_init_check(ref argc, ref argv))
{
throw new InvalidOperationException("Failed to initialize GTK. Is a display available?");
}
_window = GtkNative.gtk_window_new(0);
if (_window == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create GTK window");
}
GtkNative.gtk_window_set_title(_window, title);
GtkNative.gtk_window_set_default_size(_window, width, height);
// Create overlay container for layered content
_overlay = GtkNative.gtk_overlay_new();
GtkNative.gtk_container_add(_window, _overlay);
// Create Skia surface as base layer
_skiaSurface = new GtkSkiaSurfaceWidget(width, height);
GtkNative.gtk_container_add(_overlay, _skiaSurface.Widget);
// Create fixed container for WebView overlays
_webViewLayer = GtkNative.gtk_fixed_new();
GtkNative.gtk_overlay_add_overlay(_overlay, _webViewLayer);
GtkNative.gtk_widget_set_can_focus(_webViewLayer, canFocus: false);
GtkNative.gtk_overlay_set_overlay_pass_through(_overlay, _webViewLayer, passThrough: true);
// Store delegates to prevent garbage collection
_deleteEventHandler = OnDeleteEvent;
_configureEventHandler = OnConfigureEvent;
_buttonPressHandler = OnButtonPress;
_buttonReleaseHandler = OnButtonRelease;
_motionHandler = OnMotion;
// Connect event handlers
ConnectSignal(_window, "delete-event", Marshal.GetFunctionPointerForDelegate(_deleteEventHandler));
ConnectSignal(_window, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureEventHandler));
// Add pointer event masks
GtkNative.gtk_widget_add_events(_window, 772);
ConnectSignal(_window, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressHandler));
ConnectSignal(_window, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseHandler));
ConnectSignal(_window, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionHandler));
Console.WriteLine($"[GtkHostWindow] Created GTK window on X11: {width}x{height}");
}
private void ConnectSignal(IntPtr widget, string signal, IntPtr handler)
{
GtkNative.g_signal_connect_data(widget, signal, handler, IntPtr.Zero, IntPtr.Zero, 0);
}
private bool OnDeleteEvent(IntPtr widget, IntPtr eventData, IntPtr userData)
{
CloseRequested?.Invoke(this, EventArgs.Empty);
_isRunning = false;
GtkNative.gtk_main_quit();
return true;
}
private bool OnConfigureEvent(IntPtr widget, IntPtr eventData, IntPtr userData)
{
GtkNative.gtk_window_get_size(_window, out var width, out var height);
if (width != _width || height != _height)
{
_width = width;
_height = height;
_skiaSurface?.Resize(width, height);
Resized?.Invoke(this, (_width, _height));
}
return false;
}
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y, button) = ParseButtonEvent(eventData);
string buttonName = button switch
{
3 => "Right",
2 => "Middle",
1 => "Left",
_ => $"Other({button})",
};
Console.WriteLine($"[GtkHostWindow] ButtonPress at ({x:F1}, {y:F1}), button={button} ({buttonName})");
PointerPressed?.Invoke(this, (x, y, button));
_skiaSurface?.RaisePointerPressed(x, y, button);
return false;
}
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y, button) = ParseButtonEvent(eventData);
PointerReleased?.Invoke(this, (x, y, button));
_skiaSurface?.RaisePointerReleased(x, y, button);
return false;
}
private bool OnMotion(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (x, y) = ParseMotionEvent(eventData);
PointerMoved?.Invoke(this, (x, y));
_skiaSurface?.RaisePointerMoved(x, y);
return false;
}
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);
}
public void Show()
{
GtkNative.gtk_widget_show_all(_window);
_isRunning = true;
}
public void Hide()
{
GtkNative.gtk_widget_hide(_window);
}
public void SetTitle(string title)
{
GtkNative.gtk_window_set_title(_window, title);
}
public void SetIcon(string iconPath)
{
if (string.IsNullOrEmpty(iconPath) || !File.Exists(iconPath))
{
Console.WriteLine("[GtkHostWindow] Icon file not found: " + iconPath);
return;
}
try
{
IntPtr pixbuf = GtkNative.gdk_pixbuf_new_from_file(iconPath, IntPtr.Zero);
if (pixbuf != IntPtr.Zero)
{
GtkNative.gtk_window_set_icon(_window, pixbuf);
GtkNative.g_object_unref(pixbuf);
Console.WriteLine("[GtkHostWindow] Set window icon: " + iconPath);
}
}
catch (Exception ex)
{
Console.WriteLine("[GtkHostWindow] Failed to set icon: " + ex.Message);
}
}
public void Resize(int width, int height)
{
GtkNative.gtk_window_resize(_window, width, height);
}
public void AddWebView(IntPtr webViewWidget, int x, int y, int width, int height)
{
GtkNative.gtk_widget_set_size_request(webViewWidget, width, height);
GtkNative.gtk_fixed_put(_webViewLayer, webViewWidget, x, y);
GtkNative.gtk_widget_show(webViewWidget);
Console.WriteLine($"[GtkHostWindow] Added WebView at ({x}, {y}) size {width}x{height}");
}
public void MoveResizeWebView(IntPtr webViewWidget, int x, int y, int width, int height)
{
GtkNative.gtk_widget_set_size_request(webViewWidget, width, height);
GtkNative.gtk_fixed_move(_webViewLayer, webViewWidget, x, y);
}
public void RemoveWebView(IntPtr webViewWidget)
{
GtkNative.gtk_container_remove(_webViewLayer, webViewWidget);
}
public void RequestRedraw()
{
if (_skiaSurface != null)
{
GtkNative.gtk_widget_queue_draw(_skiaSurface.Widget);
}
}
public void Run()
{
Show();
GtkNative.gtk_main();
}
public void Stop()
{
_isRunning = false;
GtkNative.gtk_main_quit();
}
public void ProcessEvents()
{
while (GtkNative.gtk_events_pending())
{
GtkNative.gtk_main_iteration_do(blocking: false);
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_skiaSurface?.Dispose();
if (_window != IntPtr.Zero)
{
GtkNative.gtk_widget_destroy(_window);
_window = IntPtr.Zero;
}
}
}
}

View File

@@ -21,6 +21,13 @@ public class X11Window : IDisposable
private int _width; private int _width;
private int _height; private int _height;
// Cursor handles
private IntPtr _arrowCursor;
private IntPtr _handCursor;
private IntPtr _textCursor;
private IntPtr _currentCursor;
private CursorType _currentCursorType = CursorType.Arrow;
/// <summary> /// <summary>
/// Gets the native display handle. /// Gets the native display handle.
/// </summary> /// </summary>
@@ -155,7 +162,97 @@ public class X11Window : IDisposable
// Set up WM_DELETE_WINDOW protocol for proper close handling // Set up WM_DELETE_WINDOW protocol for proper close handling
_wmDeleteMessage = X11.XInternAtom(_display, "WM_DELETE_WINDOW", false); _wmDeleteMessage = X11.XInternAtom(_display, "WM_DELETE_WINDOW", false);
// Would need XSetWMProtocols here, simplified for now // Initialize cursors
_arrowCursor = X11.XCreateFontCursor(_display, 68); // XC_left_ptr
_handCursor = X11.XCreateFontCursor(_display, 60); // XC_hand2
_textCursor = X11.XCreateFontCursor(_display, 152); // XC_xterm
_currentCursor = _arrowCursor;
}
/// <summary>
/// Sets the cursor type for this window.
/// </summary>
public void SetCursor(CursorType cursorType)
{
if (_currentCursorType != cursorType)
{
_currentCursorType = cursorType;
IntPtr cursor = cursorType switch
{
CursorType.Hand => _handCursor,
CursorType.Text => _textCursor,
_ => _arrowCursor,
};
if (cursor != _currentCursor)
{
_currentCursor = cursor;
X11.XDefineCursor(_display, _window, _currentCursor);
X11.XFlush(_display);
}
}
}
/// <summary>
/// Sets the window icon from a file.
/// </summary>
public unsafe void SetIcon(string iconPath)
{
if (string.IsNullOrEmpty(iconPath) || !System.IO.File.Exists(iconPath))
{
Console.WriteLine("[X11Window] Icon file not found: " + iconPath);
return;
}
Console.WriteLine("[X11Window] SetIcon called: " + iconPath);
try
{
SkiaSharp.SKBitmap? bitmap = SkiaSharp.SKBitmap.Decode(iconPath);
if (bitmap == null)
{
Console.WriteLine("[X11Window] Failed to load icon: " + iconPath);
return;
}
Console.WriteLine($"[X11Window] Loaded bitmap: {bitmap.Width}x{bitmap.Height}");
// Scale to 64x64 if needed
int targetSize = 64;
if (bitmap.Width != targetSize || bitmap.Height != targetSize)
{
var scaled = new SkiaSharp.SKBitmap(targetSize, targetSize, false);
bitmap.ScalePixels(scaled, SkiaSharp.SKFilterQuality.High);
bitmap.Dispose();
bitmap = scaled;
}
int width = bitmap.Width;
int height = bitmap.Height;
int dataSize = 2 + width * height;
uint[] iconData = new uint[dataSize];
iconData[0] = (uint)width;
iconData[1] = (uint)height;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var pixel = bitmap.GetPixel(x, y);
iconData[2 + y * width + x] = (uint)((pixel.Alpha << 24) | (pixel.Red << 16) | (pixel.Green << 8) | pixel.Blue);
}
}
bitmap.Dispose();
IntPtr property = X11.XInternAtom(_display, "_NET_WM_ICON", false);
IntPtr type = X11.XInternAtom(_display, "CARDINAL", false);
fixed (uint* data = iconData)
{
X11.XChangeProperty(_display, _window, property, type, 32, 0, (nint)data, dataSize);
}
X11.XFlush(_display);
Console.WriteLine($"[X11Window] Set window icon: {width}x{height}");
}
catch (Exception ex)
{
Console.WriteLine("[X11Window] Failed to set icon: " + ex.Message);
}
} }
/// <summary> /// <summary>