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:
76
Dispatching/LinuxDispatcher.cs
Normal file
76
Dispatching/LinuxDispatcher.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Dispatching/LinuxDispatcherProvider.cs
Normal file
15
Dispatching/LinuxDispatcherProvider.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Dispatching/LinuxDispatcherTimer.cs
Normal file
109
Dispatching/LinuxDispatcherTimer.cs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.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
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.
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +88,18 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
123
Hosting/MauiHandlerExtensions.cs
Normal file
123
Hosting/MauiHandlerExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,84 +219,99 @@ 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();
|
||||||
linuxApp.Initialize(options);
|
try
|
||||||
|
|
||||||
// Create MAUI context
|
|
||||||
var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp);
|
|
||||||
|
|
||||||
// Get the application and render it
|
|
||||||
var application = app.Services.GetService<IApplication>();
|
|
||||||
SkiaView? rootView = null;
|
|
||||||
|
|
||||||
if (application is Microsoft.Maui.Controls.Application mauiApplication)
|
|
||||||
{
|
{
|
||||||
// Force Application.Current to be this instance
|
linuxApp.Initialize(options);
|
||||||
// The constructor sets Current = this, but we ensure it here
|
|
||||||
var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current");
|
// Create MAUI context
|
||||||
if (currentProperty != null && currentProperty.CanWrite)
|
var mauiContext = new LinuxMauiContext(app.Services, linuxApp);
|
||||||
|
|
||||||
|
// Get the application and render it
|
||||||
|
var application = app.Services.GetService<IApplication>();
|
||||||
|
SkiaView? rootView = null;
|
||||||
|
|
||||||
|
if (application is Application mauiApplication)
|
||||||
{
|
{
|
||||||
currentProperty.SetValue(null, mauiApplication);
|
// Force Application.Current to be this instance
|
||||||
|
var currentProperty = typeof(Application).GetProperty("Current");
|
||||||
|
if (currentProperty != null && currentProperty.CanWrite)
|
||||||
|
{
|
||||||
|
currentProperty.SetValue(null, mauiApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle theme changes
|
||||||
|
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == "UserAppTheme")
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Theme changed to: {mauiApplication.UserAppTheme}");
|
||||||
|
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
||||||
|
linuxApp._renderingEngine?.InvalidateAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mauiApplication.MainPage != null)
|
||||||
|
{
|
||||||
|
var mainPage = mauiApplication.MainPage;
|
||||||
|
|
||||||
|
var windowsField = typeof(Application).GetField("_windows",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
|
||||||
|
|
||||||
|
if (windowsList != null && windowsList.Count == 0)
|
||||||
|
{
|
||||||
|
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
|
||||||
|
windowsList.Add(mauiWindow);
|
||||||
|
mauiWindow.Parent = mauiApplication;
|
||||||
|
}
|
||||||
|
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
|
||||||
|
{
|
||||||
|
windowsList[0].Page = mainPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderer = new LinuxViewRenderer(mauiContext);
|
||||||
|
rootView = renderer.RenderPage(mainPage);
|
||||||
|
|
||||||
|
string windowTitle = "OpenMaui App";
|
||||||
|
if (mainPage is NavigationPage navPage)
|
||||||
|
{
|
||||||
|
windowTitle = navPage.Title ?? windowTitle;
|
||||||
|
}
|
||||||
|
else if (mainPage is Shell shell)
|
||||||
|
{
|
||||||
|
windowTitle = shell.Title ?? windowTitle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
windowTitle = mainPage.Title ?? windowTitle;
|
||||||
|
}
|
||||||
|
linuxApp.SetWindowTitle(windowTitle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mauiApplication.MainPage != null)
|
if (rootView == null)
|
||||||
{
|
{
|
||||||
// Create a MAUI Window and add it to the application
|
rootView = LinuxProgramHost.CreateDemoView();
|
||||||
// This ensures Shell.Current works (it reads from Application.Current.Windows[0].Page)
|
|
||||||
var mainPage = mauiApplication.MainPage;
|
|
||||||
|
|
||||||
// Always ensure we have a window with the Shell/Page
|
|
||||||
var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows",
|
|
||||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
||||||
var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List<Microsoft.Maui.Controls.Window>;
|
|
||||||
|
|
||||||
if (windowsList != null && windowsList.Count == 0)
|
|
||||||
{
|
|
||||||
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
|
|
||||||
windowsList.Add(mauiWindow);
|
|
||||||
mauiWindow.Parent = mauiApplication;
|
|
||||||
}
|
|
||||||
else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null)
|
|
||||||
{
|
|
||||||
// Window exists but has no page - set it
|
|
||||||
windowsList[0].Page = mainPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
var renderer = new Hosting.LinuxViewRenderer(mauiContext);
|
|
||||||
rootView = renderer.RenderPage(mainPage);
|
|
||||||
|
|
||||||
// Update window title based on app name (NavigationPage.Title takes precedence)
|
|
||||||
string windowTitle = "OpenMaui App";
|
|
||||||
if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage)
|
|
||||||
{
|
|
||||||
// Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title
|
|
||||||
windowTitle = navPage.Title ?? windowTitle;
|
|
||||||
}
|
|
||||||
else if (mainPage is Microsoft.Maui.Controls.Shell shell)
|
|
||||||
{
|
|
||||||
windowTitle = shell.Title ?? windowTitle;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
windowTitle = mainPage.Title ?? windowTitle;
|
|
||||||
}
|
|
||||||
linuxApp.SetWindowTitle(windowTitle);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to demo if no view
|
linuxApp.RootView = rootView;
|
||||||
if (rootView == null)
|
linuxApp.Run();
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
rootView = Hosting.LinuxProgramHost.CreateDemoView();
|
linuxApp?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
linuxApp.RootView = rootView;
|
|
||||||
linuxApp.Run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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
80
Native/CairoNative.cs
Normal 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
111
Native/GLibNative.cs
Normal 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
132
Native/GdkNative.cs
Normal 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
192
Native/GtkNative.cs
Normal 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
256
Native/WebKitNative.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
391
Rendering/GtkSkiaSurfaceWidget.cs
Normal file
391
Rendering/GtkSkiaSurfaceWidget.cs
Normal 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
142
Services/AppInfoService.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
Services/ConnectivityService.cs
Normal file
170
Services/ConnectivityService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
Services/DeviceDisplayService.cs
Normal file
124
Services/DeviceDisplayService.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Services/DeviceInfoService.cs
Normal file
93
Services/DeviceInfoService.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
Services/GtkContextMenuService.cs
Normal file
90
Services/GtkContextMenuService.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Services/GtkHostService.cs
Normal file
56
Services/GtkHostService.cs
Normal 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
32
Services/GtkMenuItem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
158
Services/MauiIconGenerator.cs
Normal file
158
Services/MauiIconGenerator.cs
Normal 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
101
Views/LinuxDialogService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,53 +564,151 @@ 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(
|
||||||
|
IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
||||||
|
SKFontStyleWidth.Normal,
|
||||||
|
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||||
|
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
||||||
|
?? SKTypeface.Default;
|
||||||
|
|
||||||
|
using var font = new SKFont(typeface, FontSize);
|
||||||
|
|
||||||
|
// Determine text color
|
||||||
|
SKColor textColorToUse;
|
||||||
|
if (!IsEnabled)
|
||||||
{
|
{
|
||||||
var fontStyle = new SKFontStyle(
|
textColorToUse = TextColor.WithAlpha(128);
|
||||||
IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
}
|
||||||
SKFontStyleWidth.Normal,
|
else if (isTextOnly && (IsHovered || IsPressed))
|
||||||
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
{
|
||||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
textColorToUse = new SKColor(
|
||||||
?? SKTypeface.Default;
|
(byte)Math.Max(0, TextColor.Red - 40),
|
||||||
|
(byte)Math.Max(0, TextColor.Green - 40),
|
||||||
|
(byte)Math.Max(0, TextColor.Blue - 40),
|
||||||
|
TextColor.Alpha);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textColorToUse = TextColor;
|
||||||
|
}
|
||||||
|
|
||||||
using var font = new SKFont(typeface, FontSize);
|
using var textPaint = new SKPaint(font)
|
||||||
|
{
|
||||||
|
Color = textColorToUse,
|
||||||
|
IsAntialias = true
|
||||||
|
};
|
||||||
|
|
||||||
// For text-only buttons, darken text on hover/press for feedback
|
// Measure text
|
||||||
SKColor textColorToUse;
|
var textBounds = new SKRect();
|
||||||
if (!IsEnabled)
|
bool hasText = !string.IsNullOrEmpty(Text);
|
||||||
|
if (hasText)
|
||||||
|
{
|
||||||
|
textPaint.MeasureText(Text, ref textBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
textColorToUse = TextColor.WithAlpha(128);
|
totalWidth = imageWidth + ImageSpacing + textBounds.Width;
|
||||||
}
|
totalHeight = Math.Max(imageHeight, textBounds.Height);
|
||||||
else if (isTextOnly && (IsHovered || IsPressed))
|
|
||||||
{
|
|
||||||
// Darken the text color slightly for hover/press feedback
|
|
||||||
textColorToUse = new SKColor(
|
|
||||||
(byte)Math.Max(0, TextColor.Red - 40),
|
|
||||||
(byte)Math.Max(0, TextColor.Green - 40),
|
|
||||||
(byte)Math.Max(0, TextColor.Blue - 40),
|
|
||||||
TextColor.Alpha);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
textColorToUse = TextColor;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var paint = new SKPaint(font)
|
// Draw image
|
||||||
|
var imageRect = new SKRect(imageX, imageY, imageX + imageWidth, imageY + imageHeight);
|
||||||
|
using var imagePaint = new SKPaint { IsAntialias = true };
|
||||||
|
if (!IsEnabled)
|
||||||
{
|
{
|
||||||
Color = textColorToUse,
|
imagePaint.ColorFilter = SKColorFilter.CreateBlendMode(new SKColor(128, 128, 128, 128), SKBlendMode.Modulate);
|
||||||
IsAntialias = true
|
}
|
||||||
};
|
canvas.DrawBitmap(ImageSource!, imageRect, imagePaint);
|
||||||
|
|
||||||
// Measure text
|
// Draw text
|
||||||
var textBounds = new SKRect();
|
if (hasText)
|
||||||
paint.MeasureText(Text, ref textBounds);
|
{
|
||||||
|
canvas.DrawText(Text!, textX, textY, textPaint);
|
||||||
// Center text
|
}
|
||||||
|
}
|
||||||
|
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
221
Views/SkiaContextMenu.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
256
Views/SkiaFlexLayout.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) { }
|
||||||
|
|||||||
@@ -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
11
Window/CursorType.cs
Normal 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
343
Window/GtkHostWindow.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user