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:
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 _height;
|
||||
|
||||
// Cursor handles
|
||||
private IntPtr _arrowCursor;
|
||||
private IntPtr _handCursor;
|
||||
private IntPtr _textCursor;
|
||||
private IntPtr _currentCursor;
|
||||
private CursorType _currentCursorType = CursorType.Arrow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the native display handle.
|
||||
/// </summary>
|
||||
@@ -155,7 +162,97 @@ public class X11Window : IDisposable
|
||||
// Set up WM_DELETE_WINDOW protocol for proper close handling
|
||||
_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>
|
||||
|
||||
Reference in New Issue
Block a user