Files
logikonline 3412cb982e
All checks were successful
CI / Build (Linux) (push) Successful in 21s
fix(interop): resolve native resource leaks in GTK and WebKit interop
Fix critical memory leaks identified in architecture review: Add signal handler disconnection in WebKitNative (load-changed and script-dialog signals now properly cleaned up), implement GTK idle callback cleanup with automatic removal on completion, add dlclose() calls for WebKit library handles, track GTK signal IDs in GtkSkiaSurfaceWidget for proper disposal. Replace empty catch blocks in GestureManager with logged exception handling. Add WebKitNative.Cleanup() and GtkNative.ClearCallbacks() methods for application shutdown.
2026-03-06 23:14:53 -05:00

417 lines
15 KiB
C#

using System;
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Services;
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 ulong _buttonPressSignalId;
private ulong _buttonReleaseSignalId;
private ulong _motionSignalId;
private ulong _keyPressSignalId;
private ulong _keyReleaseSignalId;
private ulong _scrollSignalId;
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, uint State)>? 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);
_buttonPressSignalId = GtkNative.g_signal_connect_data(_widget, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
_buttonReleaseSignalId = GtkNative.g_signal_connect_data(_widget, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
_motionSignalId = GtkNative.g_signal_connect_data(_widget, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionCallback), IntPtr.Zero, IntPtr.Zero, 0);
_keyPressSignalId = GtkNative.g_signal_connect_data(_widget, "key-press-event", Marshal.GetFunctionPointerForDelegate(_keyPressCallback), IntPtr.Zero, IntPtr.Zero, 0);
_keyReleaseSignalId = GtkNative.g_signal_connect_data(_widget, "key-release-event", Marshal.GetFunctionPointerForDelegate(_keyReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0);
_scrollSignalId = GtkNative.g_signal_connect_data(_widget, "scroll-event", Marshal.GetFunctionPointerForDelegate(_scrollCallback), IntPtr.Zero, IntPtr.Zero, 0);
DiagnosticLog.Debug("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);
DiagnosticLog.Debug("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, eventType) = ParseButtonEvent(eventData);
// GTK event types: GDK_BUTTON_PRESS=4, GDK_2BUTTON_PRESS=5, GDK_3BUTTON_PRESS=6
// Only process single button press events. GTK sends 2BUTTON_PRESS and 3BUTTON_PRESS
// events after detecting double/triple clicks, but we handle that ourselves in SkiaEntry.
if (eventType != 4) // GDK_BUTTON_PRESS
return true;
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)
{
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();
DiagnosticLog.Debug("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, state) = ParseScrollEvent(eventData);
Scrolled?.Invoke(this, (x, y, deltaX, deltaY, state));
return true;
}
private static (double x, double y, int button, int eventType) ParseButtonEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventButton>(eventData);
return (evt.x, evt.y, (int)evt.button, evt.type);
}
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, uint state) 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, evt.state);
}
public void GrabFocus()
{
GtkNative.gtk_widget_grab_focus(_widget);
}
public void Dispose()
{
// Disconnect all signal handlers before disposing
if (_widget != IntPtr.Zero)
{
if (_drawSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _drawSignalId);
if (_configureSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _configureSignalId);
if (_buttonPressSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _buttonPressSignalId);
if (_buttonReleaseSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _buttonReleaseSignalId);
if (_motionSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _motionSignalId);
if (_keyPressSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _keyPressSignalId);
if (_keyReleaseSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _keyReleaseSignalId);
if (_scrollSignalId != 0) GtkNative.g_signal_handler_disconnect(_widget, _scrollSignalId);
}
_canvas?.Dispose();
_canvas = null;
_bitmap?.Dispose();
_bitmap = null;
if (_cairoSurface != IntPtr.Zero)
{
CairoNative.cairo_surface_destroy(_cairoSurface);
_cairoSurface = IntPtr.Zero;
}
}
}