Files
maui-linux/Window/GtkHostWindow.cs
Dave Friedel 1f096c38dc Update with recovered code from VM binaries (Jan 1)
Recovered from decompiled OpenMaui.Controls.Linux.dll:
- SkiaShell.cs: FlyoutHeader, FlyoutFooter, scroll support (918 -> 1325 lines)
- X11Window.cs: Cursor support (XCreateFontCursor, XDefineCursor)
- All handlers with dark mode support
- All services with latest implementations
- LinuxApplication with theme change handling
2026-01-01 06:22:48 -05:00

346 lines
9.3 KiB
C#

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;
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;
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);
_overlay = GtkNative.gtk_overlay_new();
GtkNative.gtk_container_add(_window, _overlay);
_skiaSurface = new GtkSkiaSurfaceWidget(width, height);
GtkNative.gtk_container_add(_overlay, _skiaSurface.Widget);
_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);
_deleteEventHandler = OnDeleteEvent;
_configureEventHandler = OnConfigureEvent;
_buttonPressHandler = OnButtonPress;
_buttonReleaseHandler = OnButtonRelease;
_motionHandler = OnMotion;
ConnectSignal(_window, "delete-event", Marshal.GetFunctionPointerForDelegate(_deleteEventHandler));
ConnectSignal(_window, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureEventHandler));
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)
{
this.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);
this.Resized?.Invoke(this, (_width, _height));
}
return false;
}
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
{
(double x, double y, int button) tuple = ParseButtonEvent(eventData);
double item = tuple.x;
double item2 = tuple.y;
int item3 = tuple.button;
string value = item3 switch
{
3 => "Right",
2 => "Middle",
1 => "Left",
_ => $"Other({item3})",
};
Console.WriteLine($"[GtkHostWindow] ButtonPress at ({item:F1}, {item2:F1}), button={item3} ({value})");
this.PointerPressed?.Invoke(this, (item, item2, item3));
_skiaSurface?.RaisePointerPressed(item, item2, item3);
return false;
}
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (num, num2, num3) = ParseButtonEvent(eventData);
this.PointerReleased?.Invoke(this, (num, num2, num3));
_skiaSurface?.RaisePointerReleased(num, num2, num3);
return false;
}
private bool OnMotion(IntPtr widget, IntPtr eventData, IntPtr userData)
{
var (num, num2) = ParseMotionEvent(eventData);
this.PointerMoved?.Invoke(this, (num, num2));
_skiaSurface?.RaisePointerMoved(num, num2);
return false;
}
private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData)
{
GdkEventButton gdkEventButton = Marshal.PtrToStructure<GdkEventButton>(eventData);
return (x: gdkEventButton.x, y: gdkEventButton.y, button: (int)gdkEventButton.button);
}
private static (double x, double y) ParseMotionEvent(IntPtr eventData)
{
GdkEventMotion gdkEventMotion = Marshal.PtrToStructure<GdkEventMotion>(eventData);
return (x: gdkEventMotion.x, y: gdkEventMotion.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 intPtr = GtkNative.gdk_pixbuf_new_from_file(iconPath, IntPtr.Zero);
if (intPtr != IntPtr.Zero)
{
GtkNative.gtk_window_set_icon(_window, intPtr);
GtkNative.g_object_unref(intPtr);
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;
}
}
}
}