Files
maui-linux/Rendering/GtkSkiaSurfaceWidget.cs

417 lines
15 KiB
C#
Raw Normal View History

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;
2026-01-17 02:48:59 +00:00
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);
2026-01-17 08:51:13 +00:00
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)
{
2026-01-17 08:51:13 +00:00
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)
{
2026-01-17 02:48:59 +00:00
var (x, y, deltaX, deltaY, state) = ParseScrollEvent(eventData);
Scrolled?.Invoke(this, (x, y, deltaX, deltaY, state));
return true;
}
2026-01-17 08:51:13 +00:00
private static (double x, double y, int button, int eventType) ParseButtonEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure<GdkEventButton>(eventData);
2026-01-17 08:51:13 +00:00
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);
}
2026-01-17 02:48:59 +00:00
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;
}
}
2026-01-17 02:48:59 +00:00
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;
}
}
}