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:
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -68,7 +69,7 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
||||
if (content.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}");
|
||||
content.Handler = content.ToHandler(handler.MauiContext);
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
|
||||
[nameof(ICheckBox.IsChecked)] = MapIsChecked,
|
||||
[nameof(ICheckBox.Foreground)] = MapForeground,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||
};
|
||||
@@ -86,6 +87,12 @@ public partial class CheckBoxHandler : ViewHandler<ICheckBox, SkiaCheckBox>
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = checkBox.IsEnabled;
|
||||
}
|
||||
|
||||
public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -158,7 +159,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
// Create handler for the view
|
||||
if (view.Handler == null && handler.MauiContext != null)
|
||||
{
|
||||
view.Handler = view.ToHandler(handler.MauiContext);
|
||||
view.Handler = view.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (view.Handler?.PlatformView is SkiaView skiaView)
|
||||
@@ -174,7 +175,7 @@ public partial class CollectionViewHandler : ViewHandler<CollectionView, SkiaCol
|
||||
{
|
||||
if (cellView.Handler == null && handler.MauiContext != null)
|
||||
{
|
||||
cellView.Handler = cellView.ToHandler(handler.MauiContext);
|
||||
cellView.Handler = cellView.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (cellView.Handler?.PlatformView is SkiaView skiaView)
|
||||
|
||||
@@ -49,6 +49,17 @@ public partial class DatePickerHandler : ViewHandler<IDatePicker, SkiaDatePicker
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
platformView.DateSelected += OnDateSelected;
|
||||
|
||||
// Apply dark theme colors if dark mode is active
|
||||
var current = Application.Current;
|
||||
if (current != null && (int)current.UserAppTheme == 2) // Dark theme
|
||||
{
|
||||
platformView.CalendarBackgroundColor = new SKColor(30, 30, 30);
|
||||
platformView.TextColor = new SKColor(224, 224, 224);
|
||||
platformView.BorderColor = new SKColor(97, 97, 97);
|
||||
platformView.DisabledDayColor = new SKColor(97, 97, 97);
|
||||
platformView.BackgroundColor = new SKColor(45, 45, 45);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaDatePicker platformView)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -31,6 +32,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -212,4 +214,17 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(EntryHandler handler, IEntry entry)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (entry is Entry ve && ve.BackgroundColor != null)
|
||||
{
|
||||
Console.WriteLine($"[EntryHandler] MapBackgroundColor: {ve.BackgroundColor}");
|
||||
var color = ve.BackgroundColor.ToSKColor();
|
||||
Console.WriteLine($"[EntryHandler] Setting EntryBackgroundColor to: {color}");
|
||||
handler.PlatformView.EntryBackgroundColor = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
Handlers/FlexLayoutHandler.cs
Normal file
105
Handlers/FlexLayoutHandler.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -92,7 +93,7 @@ public partial class FrameHandler : ViewHandler<Frame, SkiaFrame>
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
content.Handler = content.ToHandler(handler.MauiContext);
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
|
||||
439
Handlers/GestureManager.cs
Normal file
439
Handlers/GestureManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
60
Handlers/GtkWebViewManager.cs
Normal file
60
Handlers/GtkWebViewManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
164
Handlers/GtkWebViewPlatformView.cs
Normal file
164
Handlers/GtkWebViewPlatformView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using SkiaSharp;
|
||||
@@ -20,6 +22,8 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
[nameof(IImage.IsOpaque)] = MapIsOpaque,
|
||||
[nameof(IImageSourcePart.Source)] = MapSource,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["Width"] = MapWidth,
|
||||
["Height"] = MapHeight,
|
||||
};
|
||||
|
||||
public static CommandMapper<IImage, ImageHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -88,6 +92,19 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
// Extract width/height requests from Image control
|
||||
if (image is Image img)
|
||||
{
|
||||
if (img.WidthRequest > 0)
|
||||
{
|
||||
handler.PlatformView.WidthRequest = img.WidthRequest;
|
||||
}
|
||||
if (img.HeightRequest > 0)
|
||||
{
|
||||
handler.PlatformView.HeightRequest = img.HeightRequest;
|
||||
}
|
||||
}
|
||||
|
||||
handler.SourceLoader.UpdateImageSourceAsync();
|
||||
}
|
||||
|
||||
@@ -101,6 +118,36 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapWidth(ImageHandler handler, IImage image)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (image is Image img && img.WidthRequest > 0)
|
||||
{
|
||||
handler.PlatformView.WidthRequest = img.WidthRequest;
|
||||
Console.WriteLine($"[ImageHandler] MapWidth: {img.WidthRequest}");
|
||||
}
|
||||
else if (image.Width > 0)
|
||||
{
|
||||
handler.PlatformView.WidthRequest = image.Width;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapHeight(ImageHandler handler, IImage image)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (image is Image img && img.HeightRequest > 0)
|
||||
{
|
||||
handler.PlatformView.HeightRequest = img.HeightRequest;
|
||||
Console.WriteLine($"[ImageHandler] MapHeight: {img.HeightRequest}");
|
||||
}
|
||||
else if (image.Height > 0)
|
||||
{
|
||||
handler.PlatformView.HeightRequest = image.Height;
|
||||
}
|
||||
}
|
||||
|
||||
// Image source loading helper
|
||||
private ImageSourceServiceResultManager _sourceLoader = null!;
|
||||
|
||||
@@ -162,6 +209,14 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
await _handler.PlatformView!.LoadFromStreamAsync(stream);
|
||||
}
|
||||
}
|
||||
else if (source is FontImageSource fontSource)
|
||||
{
|
||||
var bitmap = RenderFontImageSource(fontSource, _handler.PlatformView!.WidthRequest, _handler.PlatformView.HeightRequest);
|
||||
if (bitmap != null)
|
||||
{
|
||||
_handler.PlatformView.LoadFromBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -176,5 +231,73 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SKBitmap? RenderFontImageSource(FontImageSource fontSource, double requestedWidth, double requestedHeight)
|
||||
{
|
||||
string glyph = fontSource.Glyph;
|
||||
if (string.IsNullOrEmpty(glyph))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int size = (int)Math.Max(requestedWidth > 0 ? requestedWidth : 24.0, requestedHeight > 0 ? requestedHeight : 24.0);
|
||||
size = Math.Max(size, 16);
|
||||
|
||||
SKColor color = fontSource.Color?.ToSKColor() ?? SKColors.Black;
|
||||
SKBitmap bitmap = new SKBitmap(size, size, false);
|
||||
using SKCanvas canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
SKTypeface? typeface = null;
|
||||
if (!string.IsNullOrEmpty(fontSource.FontFamily))
|
||||
{
|
||||
string[] fontPaths = new string[]
|
||||
{
|
||||
"/usr/share/fonts/truetype/" + fontSource.FontFamily + ".ttf",
|
||||
"/usr/share/fonts/opentype/" + fontSource.FontFamily + ".otf",
|
||||
"/usr/local/share/fonts/" + fontSource.FontFamily + ".ttf",
|
||||
Path.Combine(AppContext.BaseDirectory, fontSource.FontFamily + ".ttf")
|
||||
};
|
||||
|
||||
foreach (string path in fontPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
typeface = SKTypeface.FromFile(path, 0);
|
||||
if (typeface != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeface == null)
|
||||
{
|
||||
typeface = SKTypeface.FromFamilyName(fontSource.FontFamily);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeface == null)
|
||||
{
|
||||
typeface = SKTypeface.Default;
|
||||
}
|
||||
|
||||
float fontSize = size * 0.8f;
|
||||
using SKFont font = new SKFont(typeface, fontSize, 1f, 0f);
|
||||
using SKPaint paint = new SKPaint(font)
|
||||
{
|
||||
Color = color,
|
||||
IsAntialias = true,
|
||||
TextAlign = SKTextAlign.Center
|
||||
};
|
||||
|
||||
SKRect bounds = default;
|
||||
paint.MeasureText(glyph, ref bounds);
|
||||
float x = size / 2f;
|
||||
float y = (size - bounds.Top - bounds.Bottom) / 2f;
|
||||
canvas.DrawText(glyph, x, y, paint);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -29,6 +32,7 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment,
|
||||
[nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
|
||||
["FormattedText"] = MapFormattedText,
|
||||
};
|
||||
|
||||
public static CommandMapper<ILabel, LabelHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -49,6 +53,39 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
return new SkiaLabel();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaLabel platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
if (VirtualView is View view)
|
||||
{
|
||||
platformView.MauiView = view;
|
||||
|
||||
// Set hand cursor if the label has tap gesture recognizers
|
||||
if (view.GestureRecognizers.OfType<TapGestureRecognizer>().Any())
|
||||
{
|
||||
platformView.CursorType = CursorType.Hand;
|
||||
}
|
||||
}
|
||||
|
||||
platformView.Tapped += OnPlatformViewTapped;
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaLabel platformView)
|
||||
{
|
||||
platformView.Tapped -= OnPlatformViewTapped;
|
||||
platformView.MauiView = null;
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnPlatformViewTapped(object? sender, EventArgs e)
|
||||
{
|
||||
if (VirtualView is View view)
|
||||
{
|
||||
GestureManager.ProcessTap(view, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapText(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -205,4 +242,53 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
|
||||
_ => LayoutOptions.Start
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapFormattedText(LabelHandler handler, ILabel label)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (label is not Label mauiLabel)
|
||||
{
|
||||
handler.PlatformView.FormattedSpans = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedText = mauiLabel.FormattedText;
|
||||
if (formattedText == null || formattedText.Spans.Count == 0)
|
||||
{
|
||||
handler.PlatformView.FormattedSpans = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var spans = new List<SkiaTextSpan>();
|
||||
foreach (var span in formattedText.Spans)
|
||||
{
|
||||
var skiaSpan = new SkiaTextSpan
|
||||
{
|
||||
Text = span.Text ?? "",
|
||||
IsBold = span.FontAttributes.HasFlag(FontAttributes.Bold),
|
||||
IsItalic = span.FontAttributes.HasFlag(FontAttributes.Italic),
|
||||
IsUnderline = (span.TextDecorations & TextDecorations.Underline) != 0,
|
||||
IsStrikethrough = (span.TextDecorations & TextDecorations.Strikethrough) != 0,
|
||||
CharacterSpacing = (float)span.CharacterSpacing,
|
||||
LineHeight = (float)span.LineHeight
|
||||
};
|
||||
|
||||
if (span.TextColor != null)
|
||||
skiaSpan.TextColor = span.TextColor.ToSKColor();
|
||||
|
||||
if (span.BackgroundColor != null)
|
||||
skiaSpan.BackgroundColor = span.BackgroundColor.ToSKColor();
|
||||
|
||||
if (!string.IsNullOrEmpty(span.FontFamily))
|
||||
skiaSpan.FontFamily = span.FontFamily;
|
||||
|
||||
if (span.FontSize > 0)
|
||||
skiaSpan.FontSize = (float)span.FontSize;
|
||||
|
||||
spans.Add(skiaSpan);
|
||||
}
|
||||
|
||||
handler.PlatformView.FormattedSpans = spans;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -78,7 +79,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToHandler(MauiContext);
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
if (child.Handler?.PlatformView is SkiaView skiaChild)
|
||||
@@ -299,7 +300,7 @@ public partial class GridHandler : LayoutHandler
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToHandler(MauiContext);
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -64,7 +65,7 @@ public partial class LayoutHandler : ViewHandler<ILayout, SkiaLayoutView>
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToHandler(MauiContext);
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
// Add child's platform view to our layout
|
||||
@@ -284,7 +285,7 @@ public partial class GridHandler : LayoutHandler
|
||||
// Create handler for child if it doesn't exist
|
||||
if (child.Handler == null)
|
||||
{
|
||||
child.Handler = child.ToHandler(MauiContext);
|
||||
child.Handler = child.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
// Get grid position from attached properties
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
@@ -100,7 +101,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
if (page.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}");
|
||||
page.Handler = page.ToHandler(MauiContext);
|
||||
page.Handler = page.ToViewHandler(MauiContext);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}");
|
||||
@@ -122,7 +123,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}");
|
||||
if (contentPage.Content.Handler == null)
|
||||
{
|
||||
contentPage.Content.Handler = contentPage.Content.ToHandler(MauiContext);
|
||||
contentPage.Content.Handler = contentPage.Content.ToViewHandler(MauiContext);
|
||||
}
|
||||
if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
{
|
||||
@@ -221,7 +222,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
if (e.Page.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}");
|
||||
e.Page.Handler = e.Page.ToHandler(MauiContext);
|
||||
e.Page.Handler = e.Page.ToViewHandler(MauiContext);
|
||||
Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}");
|
||||
}
|
||||
|
||||
@@ -334,7 +335,7 @@ public partial class NavigationPageHandler : ViewHandler<NavigationPage, SkiaNav
|
||||
// Ensure handler exists
|
||||
if (page.Handler == null)
|
||||
{
|
||||
page.Handler = page.ToHandler(handler.MauiContext);
|
||||
page.Handler = page.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (page.Handler?.PlatformView is SkiaPage skiaPage)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
@@ -144,7 +145,7 @@ public partial class ContentPageHandler : PageHandler
|
||||
if (content.Handler == null)
|
||||
{
|
||||
Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}");
|
||||
content.Handler = content.ToHandler(handler.MauiContext);
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
// The content's handler should provide the platform view
|
||||
|
||||
@@ -22,6 +22,7 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
[nameof(IPicker.TitleColor)] = MapTitleColor,
|
||||
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
|
||||
[nameof(IPicker.TextColor)] = MapTextColor,
|
||||
[nameof(ITextStyle.Font)] = MapFont,
|
||||
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||
[nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||
[nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||
@@ -129,6 +130,22 @@ public partial class PickerHandler : ViewHandler<IPicker, SkiaPicker>
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapFont(PickerHandler handler, IPicker picker)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
var font = picker.Font;
|
||||
if (!string.IsNullOrEmpty(font.Family))
|
||||
{
|
||||
handler.PlatformView.FontFamily = font.Family;
|
||||
}
|
||||
if (font.Size > 0)
|
||||
{
|
||||
handler.PlatformView.FontSize = (float)font.Size;
|
||||
}
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapCharacterSpacing(PickerHandler handler, IPicker picker)
|
||||
{
|
||||
// Character spacing could be implemented with custom text rendering
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using SkiaSharp;
|
||||
@@ -18,7 +20,9 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
{
|
||||
[nameof(IProgress.Progress)] = MapProgress,
|
||||
[nameof(IProgress.ProgressColor)] = MapProgressColor,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
["BackgroundColor"] = MapBackgroundColor,
|
||||
};
|
||||
|
||||
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -39,6 +43,40 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
return new SkiaProgressBar();
|
||||
}
|
||||
|
||||
protected override void ConnectHandler(SkiaProgressBar platformView)
|
||||
{
|
||||
base.ConnectHandler(platformView);
|
||||
|
||||
if (VirtualView is BindableObject bindable)
|
||||
{
|
||||
bindable.PropertyChanged += OnVirtualViewPropertyChanged;
|
||||
}
|
||||
|
||||
if (VirtualView is VisualElement visualElement)
|
||||
{
|
||||
platformView.IsVisible = visualElement.IsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisconnectHandler(SkiaProgressBar platformView)
|
||||
{
|
||||
if (VirtualView is BindableObject bindable)
|
||||
{
|
||||
bindable.PropertyChanged -= OnVirtualViewPropertyChanged;
|
||||
}
|
||||
|
||||
base.DisconnectHandler(platformView);
|
||||
}
|
||||
|
||||
private void OnVirtualViewPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (VirtualView is VisualElement visualElement && e.PropertyName == nameof(VisualElement.IsVisible))
|
||||
{
|
||||
PlatformView.IsVisible = visualElement.IsVisible;
|
||||
PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapProgress(ProgressBarHandler handler, IProgress progress)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
@@ -50,7 +88,18 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (progress.ProgressColor is not null)
|
||||
{
|
||||
handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor();
|
||||
}
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
handler.PlatformView.IsEnabled = progress.IsEnabled;
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
|
||||
public static void MapBackground(ProgressBarHandler handler, IProgress progress)
|
||||
@@ -60,6 +109,18 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
||||
if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
|
||||
if (progress is VisualElement visualElement && visualElement.BackgroundColor is not null)
|
||||
{
|
||||
handler.PlatformView.BackgroundColor = visualElement.BackgroundColor.ToSKColor();
|
||||
handler.PlatformView.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
@@ -52,7 +53,7 @@ public partial class ScrollViewHandler : ViewHandler<IScrollView, SkiaScrollView
|
||||
// Create handler for content if it doesn't exist
|
||||
if (content.Handler == null)
|
||||
{
|
||||
content.Handler = content.ToHandler(handler.MauiContext);
|
||||
content.Handler = content.ToViewHandler(handler.MauiContext);
|
||||
}
|
||||
|
||||
if (content.Handler?.PlatformView is SkiaView skiaContent)
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
[nameof(ISwitch.TrackColor)] = MapTrackColor,
|
||||
[nameof(ISwitch.ThumbColor)] = MapThumbColor,
|
||||
[nameof(IView.Background)] = MapBackground,
|
||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||
};
|
||||
|
||||
public static CommandMapper<ISwitch, SwitchHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||
@@ -96,4 +97,10 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
|
||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
||||
}
|
||||
}
|
||||
|
||||
public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch)
|
||||
{
|
||||
if (handler.PlatformView is null) return;
|
||||
handler.PlatformView.IsEnabled = @switch.IsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user