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