maui-linux/Window/WaylandWindow.cs

1335 lines
47 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Input;
namespace Microsoft.Maui.Platform.Linux.Window;
/// <summary>
/// Native Wayland window implementation using xdg-shell protocol.
/// Provides full Wayland support without XWayland dependency.
/// </summary>
public class WaylandWindow : IDisposable
{
#region Native Interop - libwayland-client
private const string LibWaylandClient = "libwayland-client.so.0";
// Core display functions (actually exported)
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_display_connect(string? name);
[DllImport(LibWaylandClient)]
private static extern void wl_display_disconnect(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_dispatch(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_dispatch_pending(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_roundtrip(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_flush(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_get_fd(IntPtr display);
// Low-level proxy API (actually exported - used to implement protocol wrappers)
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_proxy_marshal_constructor(
IntPtr proxy, uint opcode, IntPtr iface, IntPtr arg);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_proxy_marshal_constructor_versioned(
IntPtr proxy, uint opcode, IntPtr iface, uint version, IntPtr arg);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1, int arg2, int arg3);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2, int arg3, int arg4);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, uint arg1);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode,
[MarshalAs(UnmanagedType.LPStr)] string arg1);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_proxy_marshal_array_constructor(
IntPtr proxy, uint opcode, IntPtr args, IntPtr iface);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_proxy_marshal_array_constructor_versioned(
IntPtr proxy, uint opcode, IntPtr args, IntPtr iface, uint version);
[DllImport(LibWaylandClient)]
private static extern int wl_proxy_add_listener(IntPtr proxy, IntPtr impl, IntPtr data);
[DllImport(LibWaylandClient)]
private static extern void wl_proxy_destroy(IntPtr proxy);
[DllImport(LibWaylandClient)]
private static extern uint wl_proxy_get_version(IntPtr proxy);
// Interface globals (exported as data symbols)
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_registry_interface_ptr();
// We need to load these at runtime since they're data symbols
private static IntPtr _wl_registry_interface;
private static IntPtr _wl_compositor_interface;
private static IntPtr _wl_shm_interface;
private static IntPtr _wl_shm_pool_interface;
private static IntPtr _wl_buffer_interface;
private static IntPtr _wl_surface_interface;
private static IntPtr _wl_seat_interface;
private static IntPtr _wl_pointer_interface;
private static IntPtr _wl_keyboard_interface;
// dlsym for loading interface symbols
[DllImport("libdl.so.2", EntryPoint = "dlopen")]
private static extern IntPtr dlopen(string? filename, int flags);
[DllImport("libdl.so.2", EntryPoint = "dlsym")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so.2", EntryPoint = "dlclose")]
private static extern int dlclose(IntPtr handle);
private const int RTLD_NOW = 2;
private const int RTLD_GLOBAL = 0x100;
#endregion
#region Wayland Protocol Opcodes
// wl_display opcodes
private const uint WL_DISPLAY_GET_REGISTRY = 1;
// wl_registry opcodes
private const uint WL_REGISTRY_BIND = 0;
// wl_compositor opcodes
private const uint WL_COMPOSITOR_CREATE_SURFACE = 0;
// wl_surface opcodes
private const uint WL_SURFACE_DESTROY = 0;
private const uint WL_SURFACE_ATTACH = 1;
private const uint WL_SURFACE_DAMAGE = 2;
private const uint WL_SURFACE_COMMIT = 6;
private const uint WL_SURFACE_DAMAGE_BUFFER = 9;
// wl_shm opcodes
private const uint WL_SHM_CREATE_POOL = 0;
// wl_shm_pool opcodes
private const uint WL_SHM_POOL_CREATE_BUFFER = 0;
private const uint WL_SHM_POOL_DESTROY = 1;
// wl_buffer opcodes
private const uint WL_BUFFER_DESTROY = 0;
// wl_seat opcodes
private const uint WL_SEAT_GET_POINTER = 0;
private const uint WL_SEAT_GET_KEYBOARD = 1;
// xdg_wm_base opcodes
private const uint XDG_WM_BASE_GET_XDG_SURFACE = 2;
private const uint XDG_WM_BASE_PONG = 3;
// xdg_surface opcodes
private const uint XDG_SURFACE_DESTROY = 0;
private const uint XDG_SURFACE_GET_TOPLEVEL = 1;
private const uint XDG_SURFACE_ACK_CONFIGURE = 4;
// xdg_toplevel opcodes
private const uint XDG_TOPLEVEL_DESTROY = 0;
private const uint XDG_TOPLEVEL_SET_TITLE = 2;
private const uint XDG_TOPLEVEL_SET_APP_ID = 3;
#endregion
#region Protocol Wrapper Methods
private static void LoadInterfaceSymbols()
{
if (_wl_registry_interface != IntPtr.Zero) return;
var handle = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_GLOBAL);
if (handle == IntPtr.Zero)
throw new InvalidOperationException("Failed to load libwayland-client.so.0");
_wl_registry_interface = dlsym(handle, "wl_registry_interface");
_wl_compositor_interface = dlsym(handle, "wl_compositor_interface");
_wl_shm_interface = dlsym(handle, "wl_shm_interface");
_wl_shm_pool_interface = dlsym(handle, "wl_shm_pool_interface");
_wl_buffer_interface = dlsym(handle, "wl_buffer_interface");
_wl_surface_interface = dlsym(handle, "wl_surface_interface");
_wl_seat_interface = dlsym(handle, "wl_seat_interface");
_wl_pointer_interface = dlsym(handle, "wl_pointer_interface");
_wl_keyboard_interface = dlsym(handle, "wl_keyboard_interface");
// Don't close - we need the symbols to remain valid
}
// wl_display_get_registry wrapper
private static IntPtr wl_display_get_registry(IntPtr display)
{
return wl_proxy_marshal_constructor(display, WL_DISPLAY_GET_REGISTRY,
_wl_registry_interface, IntPtr.Zero);
}
// wl_registry_add_listener wrapper
private static int wl_registry_add_listener(IntPtr registry, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(registry, listener, data);
}
// wl_registry_bind wrapper - uses special marshaling
[DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")]
private static extern IntPtr wl_proxy_marshal_flags(
IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags,
uint name, IntPtr ifaceName, uint ifaceVersion);
private static IntPtr wl_registry_bind(IntPtr registry, uint name, IntPtr iface, uint version)
{
// For registry bind, we need to use marshal_flags with the interface
return wl_proxy_marshal_flags(registry, WL_REGISTRY_BIND, iface, version, 0,
name, iface, version);
}
// wl_compositor_create_surface wrapper
private static IntPtr wl_compositor_create_surface(IntPtr compositor)
{
return wl_proxy_marshal_constructor(compositor, WL_COMPOSITOR_CREATE_SURFACE,
_wl_surface_interface, IntPtr.Zero);
}
// wl_surface methods
private static void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y)
{
wl_proxy_marshal(surface, WL_SURFACE_ATTACH, buffer, x, y);
}
private static void wl_surface_damage(IntPtr surface, int x, int y, int width, int height)
{
wl_proxy_marshal(surface, WL_SURFACE_DAMAGE, x, y, width, height);
}
private static void wl_surface_damage_buffer(IntPtr surface, int x, int y, int width, int height)
{
wl_proxy_marshal(surface, WL_SURFACE_DAMAGE_BUFFER, x, y, width, height);
}
private static void wl_surface_commit(IntPtr surface)
{
wl_proxy_marshal(surface, WL_SURFACE_COMMIT);
}
private static void wl_surface_destroy(IntPtr surface)
{
wl_proxy_marshal(surface, WL_SURFACE_DESTROY);
wl_proxy_destroy(surface);
}
// wl_shm_create_pool wrapper
[DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")]
private static extern IntPtr wl_proxy_marshal_flags_fd(
IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags,
IntPtr newId, int fd, int size);
private static IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size)
{
return wl_proxy_marshal_flags_fd(shm, WL_SHM_CREATE_POOL,
_wl_shm_pool_interface, wl_proxy_get_version(shm), 0,
IntPtr.Zero, fd, size);
}
// wl_shm_pool methods
[DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")]
private static extern IntPtr wl_proxy_marshal_flags_buffer(
IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags,
IntPtr newId, int offset, int width, int height, int stride, uint format);
private static IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format)
{
return wl_proxy_marshal_flags_buffer(pool, WL_SHM_POOL_CREATE_BUFFER,
_wl_buffer_interface, wl_proxy_get_version(pool), 0,
IntPtr.Zero, offset, width, height, stride, format);
}
private static void wl_shm_pool_destroy(IntPtr pool)
{
wl_proxy_marshal(pool, WL_SHM_POOL_DESTROY);
wl_proxy_destroy(pool);
}
// wl_buffer methods
private static void wl_buffer_destroy(IntPtr buffer)
{
wl_proxy_marshal(buffer, WL_BUFFER_DESTROY);
wl_proxy_destroy(buffer);
}
private static int wl_buffer_add_listener(IntPtr buffer, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(buffer, listener, data);
}
// wl_seat methods
private static int wl_seat_add_listener(IntPtr seat, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(seat, listener, data);
}
private static IntPtr wl_seat_get_pointer(IntPtr seat)
{
return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_POINTER,
_wl_pointer_interface, IntPtr.Zero);
}
private static IntPtr wl_seat_get_keyboard(IntPtr seat)
{
return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_KEYBOARD,
_wl_keyboard_interface, IntPtr.Zero);
}
private static int wl_pointer_add_listener(IntPtr pointer, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(pointer, listener, data);
}
private static int wl_keyboard_add_listener(IntPtr keyboard, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(keyboard, listener, data);
}
#endregion
#region xdg-shell Protocol Wrappers
private static IntPtr _xdg_wm_base_interface;
private static IntPtr _xdg_surface_interface;
private static IntPtr _xdg_toplevel_interface;
// We need to create and pin interface structures for xdg-shell
private static GCHandle _xdgWmBaseInterfaceHandle;
private static GCHandle _xdgSurfaceInterfaceHandle;
private static GCHandle _xdgToplevelInterfaceHandle;
private static IntPtr _xdgWmBaseName;
private static IntPtr _xdgSurfaceName;
private static IntPtr _xdgToplevelName;
private static void LoadXdgShellInterfaces()
{
if (_xdg_wm_base_interface != IntPtr.Zero) return;
// xdg-shell interfaces are NOT in libwayland-client
// We need to create minimal interface structs ourselves
// The key fields are: name (string ptr), version, method_count, methods, event_count, events
// Allocate interface names
_xdgWmBaseName = Marshal.StringToHGlobalAnsi("xdg_wm_base");
_xdgSurfaceName = Marshal.StringToHGlobalAnsi("xdg_surface");
_xdgToplevelName = Marshal.StringToHGlobalAnsi("xdg_toplevel");
// Create interface structures
var wmBaseInterface = new WlInterface
{
Name = _xdgWmBaseName,
Version = 6,
MethodCount = 4, // destroy, create_positioner, get_xdg_surface, pong
Methods = IntPtr.Zero,
EventCount = 1, // ping
Events = IntPtr.Zero
};
_xdgWmBaseInterfaceHandle = GCHandle.Alloc(wmBaseInterface, GCHandleType.Pinned);
_xdg_wm_base_interface = _xdgWmBaseInterfaceHandle.AddrOfPinnedObject();
var surfaceInterface = new WlInterface
{
Name = _xdgSurfaceName,
Version = 6,
MethodCount = 5, // destroy, get_toplevel, get_popup, set_window_geometry, ack_configure
Methods = IntPtr.Zero,
EventCount = 1, // configure
Events = IntPtr.Zero
};
_xdgSurfaceInterfaceHandle = GCHandle.Alloc(surfaceInterface, GCHandleType.Pinned);
_xdg_surface_interface = _xdgSurfaceInterfaceHandle.AddrOfPinnedObject();
var toplevelInterface = new WlInterface
{
Name = _xdgToplevelName,
Version = 6,
MethodCount = 14, // destroy, set_parent, set_title, set_app_id, etc.
Methods = IntPtr.Zero,
EventCount = 4, // configure, close, configure_bounds, wm_capabilities
Events = IntPtr.Zero
};
_xdgToplevelInterfaceHandle = GCHandle.Alloc(toplevelInterface, GCHandleType.Pinned);
_xdg_toplevel_interface = _xdgToplevelInterfaceHandle.AddrOfPinnedObject();
}
private static IntPtr xdg_wm_base_get_xdg_surface(IntPtr wmBase, IntPtr surface)
{
return wl_proxy_marshal_constructor(wmBase, XDG_WM_BASE_GET_XDG_SURFACE,
_xdg_surface_interface, surface);
}
private static void xdg_wm_base_pong(IntPtr wmBase, uint serial)
{
wl_proxy_marshal(wmBase, XDG_WM_BASE_PONG, serial);
}
private static int xdg_wm_base_add_listener(IntPtr wmBase, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(wmBase, listener, data);
}
private static IntPtr xdg_surface_get_toplevel(IntPtr xdgSurface)
{
return wl_proxy_marshal_constructor(xdgSurface, XDG_SURFACE_GET_TOPLEVEL,
_xdg_toplevel_interface, IntPtr.Zero);
}
private static void xdg_surface_ack_configure(IntPtr xdgSurface, uint serial)
{
wl_proxy_marshal(xdgSurface, XDG_SURFACE_ACK_CONFIGURE, serial);
}
private static int xdg_surface_add_listener(IntPtr xdgSurface, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(xdgSurface, listener, data);
}
private static void xdg_surface_destroy(IntPtr xdgSurface)
{
wl_proxy_marshal(xdgSurface, XDG_SURFACE_DESTROY);
wl_proxy_destroy(xdgSurface);
}
private static void xdg_toplevel_set_title(IntPtr toplevel, string title)
{
wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_TITLE, title);
}
private static void xdg_toplevel_set_app_id(IntPtr toplevel, string appId)
{
wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_APP_ID, appId);
}
private static int xdg_toplevel_add_listener(IntPtr toplevel, IntPtr listener, IntPtr data)
{
return wl_proxy_add_listener(toplevel, listener, data);
}
private static void xdg_toplevel_destroy(IntPtr toplevel)
{
wl_proxy_marshal(toplevel, XDG_TOPLEVEL_DESTROY);
wl_proxy_destroy(toplevel);
}
#endregion
#region Native Interop - libc
[DllImport("libc", EntryPoint = "shm_open")]
private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode);
[DllImport("libc", EntryPoint = "shm_unlink")]
private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name);
[DllImport("libc", EntryPoint = "ftruncate")]
private static extern int ftruncate(int fd, long length);
[DllImport("libc", EntryPoint = "mmap")]
private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset);
[DllImport("libc", EntryPoint = "munmap")]
private static extern int munmap(IntPtr addr, nuint length);
[DllImport("libc", EntryPoint = "close")]
private static extern int close(int fd);
[DllImport("libc", EntryPoint = "memfd_create")]
private static extern int memfd_create([MarshalAs(UnmanagedType.LPStr)] string name, uint flags);
private const int O_RDWR = 2;
private const int O_CREAT = 64;
private const int O_EXCL = 128;
private const int PROT_READ = 1;
private const int PROT_WRITE = 2;
private const int MAP_SHARED = 1;
private const uint MFD_CLOEXEC = 1;
#endregion
#region Wayland Structures
[StructLayout(LayoutKind.Sequential)]
private struct WlInterface
{
public IntPtr Name;
public int Version;
public int MethodCount;
public IntPtr Methods;
public int EventCount;
public IntPtr Events;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlRegistryListener
{
public IntPtr Global;
public IntPtr GlobalRemove;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlSurfaceListener
{
public IntPtr Enter;
public IntPtr Leave;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlBufferListener
{
public IntPtr Release;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlSeatListener
{
public IntPtr Capabilities;
public IntPtr Name;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlPointerListener
{
public IntPtr Enter;
public IntPtr Leave;
public IntPtr Motion;
public IntPtr Button;
public IntPtr Axis;
public IntPtr Frame;
public IntPtr AxisSource;
public IntPtr AxisStop;
public IntPtr AxisDiscrete;
}
[StructLayout(LayoutKind.Sequential)]
private struct WlKeyboardListener
{
public IntPtr Keymap;
public IntPtr Enter;
public IntPtr Leave;
public IntPtr Key;
public IntPtr Modifiers;
public IntPtr RepeatInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct XdgWmBaseListener
{
public IntPtr Ping;
}
[StructLayout(LayoutKind.Sequential)]
private struct XdgSurfaceListener
{
public IntPtr Configure;
}
[StructLayout(LayoutKind.Sequential)]
private struct XdgToplevelListener
{
public IntPtr Configure;
public IntPtr Close;
}
private const uint WL_SHM_FORMAT_ARGB8888 = 0;
private const uint WL_SHM_FORMAT_XRGB8888 = 1;
// Seat capabilities
private const uint WL_SEAT_CAPABILITY_POINTER = 1;
private const uint WL_SEAT_CAPABILITY_KEYBOARD = 2;
// Pointer button states
private const uint WL_POINTER_BUTTON_STATE_RELEASED = 0;
private const uint WL_POINTER_BUTTON_STATE_PRESSED = 1;
// Linux input button codes
private const uint BTN_LEFT = 0x110;
private const uint BTN_RIGHT = 0x111;
private const uint BTN_MIDDLE = 0x112;
// Key states
private const uint WL_KEYBOARD_KEY_STATE_RELEASED = 0;
private const uint WL_KEYBOARD_KEY_STATE_PRESSED = 1;
#endregion
#region Fields
private IntPtr _display;
private IntPtr _registry;
private IntPtr _compositor;
private IntPtr _shm;
private IntPtr _seat;
private IntPtr _xdgWmBase;
private IntPtr _surface;
private IntPtr _xdgSurface;
private IntPtr _xdgToplevel;
private IntPtr _pointer;
private IntPtr _keyboard;
private IntPtr _shmPool;
private IntPtr _buffer;
private IntPtr _pixelData;
private int _shmFd = -1;
private int _bufferSize;
private int _stride;
private int _width;
private int _height;
private int _pendingWidth;
private int _pendingHeight;
private string _title;
private bool _isRunning;
private bool _disposed;
private bool _configured;
private uint _lastConfigureSerial;
// Input state
private float _pointerX;
private float _pointerY;
private uint _pointerSerial;
private uint _modifiers;
// Delegates to prevent GC
private WlRegistryListener _registryListener;
private WlSeatListener _seatListener;
private WlPointerListener _pointerListener;
private WlKeyboardListener _keyboardListener;
private XdgWmBaseListener _wmBaseListener;
private XdgSurfaceListener _xdgSurfaceListener;
private XdgToplevelListener _toplevelListener;
private WlBufferListener _bufferListener;
// GCHandles for listener structs to prevent GC
private GCHandle _registryListenerHandle;
private GCHandle _seatListenerHandle;
private GCHandle _pointerListenerHandle;
private GCHandle _keyboardListenerHandle;
private GCHandle _wmBaseListenerHandle;
private GCHandle _xdgSurfaceListenerHandle;
private GCHandle _toplevelListenerHandle;
private GCHandle _bufferListenerHandle;
private static bool _interfacesInitialized;
// GCHandles to prevent delegate collection
private GCHandle _thisHandle;
#endregion
#region Properties
public IntPtr Display => _display;
public IntPtr Surface => _surface;
public int Width => _width;
public int Height => _height;
public bool IsRunning => _isRunning;
public IntPtr PixelData => _pixelData;
public int Stride => _stride;
#endregion
#region Events
public event EventHandler<KeyEventArgs>? KeyDown;
public event EventHandler<KeyEventArgs>? KeyUp;
public event EventHandler<TextInputEventArgs>? TextInput;
public event EventHandler<PointerEventArgs>? PointerMoved;
public event EventHandler<PointerEventArgs>? PointerPressed;
public event EventHandler<PointerEventArgs>? PointerReleased;
public event EventHandler<ScrollEventArgs>? Scroll;
public event EventHandler? Exposed;
public event EventHandler<(int Width, int Height)>? Resized;
public event EventHandler? CloseRequested;
public event EventHandler? FocusGained;
public event EventHandler? FocusLost;
#endregion
#region Constructor
public WaylandWindow(string title, int width, int height)
{
_title = title;
_width = width;
_height = height;
_pendingWidth = width;
_pendingHeight = height;
InitializeInterfaces();
Initialize();
}
#endregion
#region Initialization
private static void InitializeInterfaces()
{
if (_interfacesInitialized) return;
// Load interface symbols from libwayland-client using dlsym
LoadInterfaceSymbols();
LoadXdgShellInterfaces();
_interfacesInitialized = true;
}
private void Initialize()
{
// Keep this object alive for callbacks
_thisHandle = GCHandle.Alloc(this);
// Connect to Wayland display
_display = wl_display_connect(null);
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException(
"Failed to connect to Wayland display. " +
"Ensure WAYLAND_DISPLAY is set and a compositor is running.");
}
// Get registry
_registry = wl_display_get_registry(_display);
if (_registry == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to get Wayland registry");
}
// Set up registry listener
_registryListener = new WlRegistryListener
{
Global = Marshal.GetFunctionPointerForDelegate<RegistryGlobalDelegate>(RegistryGlobal),
GlobalRemove = Marshal.GetFunctionPointerForDelegate<RegistryGlobalRemoveDelegate>(RegistryGlobalRemove)
};
_registryListenerHandle = GCHandle.Alloc(_registryListener, GCHandleType.Pinned);
wl_registry_add_listener(_registry, _registryListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
// Do initial roundtrip to get globals
wl_display_roundtrip(_display);
// Verify we got required globals
if (_compositor == IntPtr.Zero)
throw new InvalidOperationException("Wayland compositor not found");
if (_shm == IntPtr.Zero)
throw new InvalidOperationException("Wayland shm not found");
if (_xdgWmBase == IntPtr.Zero)
throw new InvalidOperationException("xdg_wm_base not found - compositor doesn't support xdg-shell");
// Create surface
_surface = wl_compositor_create_surface(_compositor);
if (_surface == IntPtr.Zero)
throw new InvalidOperationException("Failed to create Wayland surface");
// Create xdg_surface
_xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface);
if (_xdgSurface == IntPtr.Zero)
throw new InvalidOperationException("Failed to create xdg_surface");
_xdgSurfaceListener = new XdgSurfaceListener
{
Configure = Marshal.GetFunctionPointerForDelegate<XdgSurfaceConfigureDelegate>(XdgSurfaceConfigure)
};
_xdgSurfaceListenerHandle = GCHandle.Alloc(_xdgSurfaceListener, GCHandleType.Pinned);
xdg_surface_add_listener(_xdgSurface, _xdgSurfaceListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
// Create toplevel
_xdgToplevel = xdg_surface_get_toplevel(_xdgSurface);
if (_xdgToplevel == IntPtr.Zero)
throw new InvalidOperationException("Failed to create xdg_toplevel");
_toplevelListener = new XdgToplevelListener
{
Configure = Marshal.GetFunctionPointerForDelegate<XdgToplevelConfigureDelegate>(XdgToplevelConfigure),
Close = Marshal.GetFunctionPointerForDelegate<XdgToplevelCloseDelegate>(XdgToplevelClose)
};
_toplevelListenerHandle = GCHandle.Alloc(_toplevelListener, GCHandleType.Pinned);
xdg_toplevel_add_listener(_xdgToplevel, _toplevelListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
// Set title and app_id
xdg_toplevel_set_title(_xdgToplevel, _title);
xdg_toplevel_set_app_id(_xdgToplevel, "com.openmaui.app");
// Commit empty surface to get initial configure
wl_surface_commit(_surface);
wl_display_roundtrip(_display);
// Create shared memory buffer
CreateShmBuffer();
Console.WriteLine($"[Wayland] Window created: {_width}x{_height}");
}
private void CreateShmBuffer()
{
_stride = _width * 4;
_bufferSize = _stride * _height;
// Create anonymous file for shared memory
_shmFd = memfd_create("maui-buffer", MFD_CLOEXEC);
if (_shmFd < 0)
{
// Fall back to shm_open
string shmName = $"/maui-{Environment.ProcessId}-{DateTime.Now.Ticks}";
_shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0x180); // 0600
if (_shmFd >= 0)
shm_unlink(shmName);
}
if (_shmFd < 0)
throw new InvalidOperationException("Failed to create shared memory");
if (ftruncate(_shmFd, _bufferSize) < 0)
{
close(_shmFd);
throw new InvalidOperationException("Failed to resize shared memory");
}
_pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0);
if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1))
{
close(_shmFd);
throw new InvalidOperationException("Failed to mmap shared memory");
}
// Create pool and buffer
_shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize);
if (_shmPool == IntPtr.Zero)
{
munmap(_pixelData, (nuint)_bufferSize);
close(_shmFd);
throw new InvalidOperationException("Failed to create wl_shm_pool");
}
_buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, _stride, WL_SHM_FORMAT_ARGB8888);
if (_buffer == IntPtr.Zero)
{
wl_shm_pool_destroy(_shmPool);
munmap(_pixelData, (nuint)_bufferSize);
close(_shmFd);
throw new InvalidOperationException("Failed to create wl_buffer");
}
// Listen for buffer release
_bufferListener = new WlBufferListener
{
Release = Marshal.GetFunctionPointerForDelegate<BufferReleaseDelegate>(BufferRelease)
};
if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free();
_bufferListenerHandle = GCHandle.Alloc(_bufferListener, GCHandleType.Pinned);
wl_buffer_add_listener(_buffer, _bufferListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
}
private void ResizeBuffer(int newWidth, int newHeight)
{
if (newWidth == _width && newHeight == _height) return;
if (newWidth <= 0 || newHeight <= 0) return;
// Destroy old buffer
if (_buffer != IntPtr.Zero)
wl_buffer_destroy(_buffer);
if (_shmPool != IntPtr.Zero)
wl_shm_pool_destroy(_shmPool);
if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1))
munmap(_pixelData, (nuint)_bufferSize);
if (_shmFd >= 0)
close(_shmFd);
_width = newWidth;
_height = newHeight;
CreateShmBuffer();
Resized?.Invoke(this, (_width, _height));
}
#endregion
#region Callback Delegates
private delegate void RegistryGlobalDelegate(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version);
private delegate void RegistryGlobalRemoveDelegate(IntPtr data, IntPtr registry, uint name);
private delegate void SeatCapabilitiesDelegate(IntPtr data, IntPtr seat, uint capabilities);
private delegate void SeatNameDelegate(IntPtr data, IntPtr seat, IntPtr name);
private delegate void PointerEnterDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y);
private delegate void PointerLeaveDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface);
private delegate void PointerMotionDelegate(IntPtr data, IntPtr pointer, uint time, int x, int y);
private delegate void PointerButtonDelegate(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state);
private delegate void PointerAxisDelegate(IntPtr data, IntPtr pointer, uint time, uint axis, int value);
private delegate void PointerFrameDelegate(IntPtr data, IntPtr pointer);
private delegate void KeyboardKeymapDelegate(IntPtr data, IntPtr keyboard, uint format, int fd, uint size);
private delegate void KeyboardEnterDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys);
private delegate void KeyboardLeaveDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface);
private delegate void KeyboardKeyDelegate(IntPtr data, IntPtr keyboard, uint serial, uint time, uint key, uint state);
private delegate void KeyboardModifiersDelegate(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group);
private delegate void XdgWmBasePingDelegate(IntPtr data, IntPtr wmBase, uint serial);
private delegate void XdgSurfaceConfigureDelegate(IntPtr data, IntPtr xdgSurface, uint serial);
private delegate void XdgToplevelConfigureDelegate(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states);
private delegate void XdgToplevelCloseDelegate(IntPtr data, IntPtr toplevel);
private delegate void BufferReleaseDelegate(IntPtr data, IntPtr buffer);
#endregion
#region Callback Implementations
private static void RegistryGlobal(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
var interfaceName = Marshal.PtrToStringAnsi(iface);
Console.WriteLine($"[Wayland] Global: {interfaceName} v{version}");
switch (interfaceName)
{
case "wl_compositor":
window._compositor = wl_registry_bind(registry, name, _wl_compositor_interface, Math.Min(version, 4u));
break;
case "wl_shm":
window._shm = wl_registry_bind(registry, name, _wl_shm_interface, 1);
break;
case "wl_seat":
window._seat = wl_registry_bind(registry, name, _wl_seat_interface, Math.Min(version, 5u));
window.SetupSeat();
break;
case "xdg_wm_base":
window._xdgWmBase = wl_registry_bind(registry, name, _xdg_wm_base_interface, Math.Min(version, 2u));
window.SetupXdgWmBase();
break;
}
}
private static void RegistryGlobalRemove(IntPtr data, IntPtr registry, uint name)
{
// Handle global removal if needed
}
private void SetupSeat()
{
if (_seat == IntPtr.Zero) return;
_seatListener = new WlSeatListener
{
Capabilities = Marshal.GetFunctionPointerForDelegate<SeatCapabilitiesDelegate>(SeatCapabilities),
Name = Marshal.GetFunctionPointerForDelegate<SeatNameDelegate>(SeatName)
};
_seatListenerHandle = GCHandle.Alloc(_seatListener, GCHandleType.Pinned);
wl_seat_add_listener(_seat, _seatListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
}
private static void SeatCapabilities(IntPtr data, IntPtr seat, uint capabilities)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
if ((capabilities & WL_SEAT_CAPABILITY_POINTER) != 0 && window._pointer == IntPtr.Zero)
{
window._pointer = wl_seat_get_pointer(seat);
window.SetupPointer();
}
if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0 && window._keyboard == IntPtr.Zero)
{
window._keyboard = wl_seat_get_keyboard(seat);
window.SetupKeyboard();
}
}
private static void SeatName(IntPtr data, IntPtr seat, IntPtr name) { }
private void SetupPointer()
{
if (_pointer == IntPtr.Zero) return;
_pointerListener = new WlPointerListener
{
Enter = Marshal.GetFunctionPointerForDelegate<PointerEnterDelegate>(PointerEnter),
Leave = Marshal.GetFunctionPointerForDelegate<PointerLeaveDelegate>(PointerLeave),
Motion = Marshal.GetFunctionPointerForDelegate<PointerMotionDelegate>(PointerMotion),
Button = Marshal.GetFunctionPointerForDelegate<PointerButtonDelegate>(OnPointerButton),
Axis = Marshal.GetFunctionPointerForDelegate<PointerAxisDelegate>(PointerAxis),
Frame = Marshal.GetFunctionPointerForDelegate<PointerFrameDelegate>(PointerFrame),
};
_pointerListenerHandle = GCHandle.Alloc(_pointerListener, GCHandleType.Pinned);
wl_pointer_add_listener(_pointer, _pointerListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
}
private static void PointerEnter(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window._pointerSerial = serial;
window._pointerX = x / 256.0f;
window._pointerY = y / 256.0f;
}
private static void PointerLeave(IntPtr data, IntPtr pointer, uint serial, IntPtr surface) { }
private static void PointerMotion(IntPtr data, IntPtr pointer, uint time, int x, int y)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window._pointerX = x / 256.0f;
window._pointerY = y / 256.0f;
window.PointerMoved?.Invoke(window, new PointerEventArgs((int)window._pointerX, (int)window._pointerY));
}
private static void OnPointerButton(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
var ptrButton = button switch
{
BTN_LEFT => Microsoft.Maui.Platform.PointerButton.Left,
BTN_RIGHT => Microsoft.Maui.Platform.PointerButton.Right,
BTN_MIDDLE => Microsoft.Maui.Platform.PointerButton.Middle,
_ => Microsoft.Maui.Platform.PointerButton.None
};
var args = new PointerEventArgs((int)window._pointerX, (int)window._pointerY, ptrButton);
if (state == WL_POINTER_BUTTON_STATE_PRESSED)
window.PointerPressed?.Invoke(window, args);
else
window.PointerReleased?.Invoke(window, args);
}
private static void PointerAxis(IntPtr data, IntPtr pointer, uint time, uint axis, int value)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
float delta = value / 256.0f / 10.0f;
if (axis == 0) // Vertical
window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, 0, delta));
else // Horizontal
window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, delta, 0));
}
private static void PointerFrame(IntPtr data, IntPtr pointer) { }
private void SetupKeyboard()
{
if (_keyboard == IntPtr.Zero) return;
_keyboardListener = new WlKeyboardListener
{
Keymap = Marshal.GetFunctionPointerForDelegate<KeyboardKeymapDelegate>(KeyboardKeymap),
Enter = Marshal.GetFunctionPointerForDelegate<KeyboardEnterDelegate>(KeyboardEnter),
Leave = Marshal.GetFunctionPointerForDelegate<KeyboardLeaveDelegate>(KeyboardLeave),
Key = Marshal.GetFunctionPointerForDelegate<KeyboardKeyDelegate>(KeyboardKey),
Modifiers = Marshal.GetFunctionPointerForDelegate<KeyboardModifiersDelegate>(KeyboardModifiers),
};
_keyboardListenerHandle = GCHandle.Alloc(_keyboardListener, GCHandleType.Pinned);
wl_keyboard_add_listener(_keyboard, _keyboardListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
}
private static void KeyboardKeymap(IntPtr data, IntPtr keyboard, uint format, int fd, uint size)
{
close(fd);
}
private static void KeyboardEnter(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window.FocusGained?.Invoke(window, EventArgs.Empty);
}
private static void KeyboardLeave(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window.FocusLost?.Invoke(window, EventArgs.Empty);
}
private static void KeyboardKey(IntPtr data, IntPtr keyboard, uint serial, uint time, uint keycode, uint state)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
// Convert Linux keycode to Key enum (add 8 for X11 compat)
var key = KeyMapping.FromLinuxKeycode(keycode + 8);
var modifiers = (KeyModifiers)window._modifiers;
var args = new KeyEventArgs(key, modifiers);
if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
{
window.KeyDown?.Invoke(window, args);
// Generate text input for printable keys
char? ch = KeyMapping.ToChar(key, modifiers);
if (ch.HasValue)
window.TextInput?.Invoke(window, new TextInputEventArgs(ch.Value.ToString()));
}
else
{
window.KeyUp?.Invoke(window, args);
}
}
private static void KeyboardModifiers(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window._modifiers = modsDepressed | modsLatched;
}
private void SetupXdgWmBase()
{
if (_xdgWmBase == IntPtr.Zero) return;
_wmBaseListener = new XdgWmBaseListener
{
Ping = Marshal.GetFunctionPointerForDelegate<XdgWmBasePingDelegate>(XdgWmBasePing)
};
_wmBaseListenerHandle = GCHandle.Alloc(_wmBaseListener, GCHandleType.Pinned);
xdg_wm_base_add_listener(_xdgWmBase, _wmBaseListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle));
}
private static void XdgWmBasePing(IntPtr data, IntPtr wmBase, uint serial)
{
xdg_wm_base_pong(wmBase, serial);
}
private static void XdgSurfaceConfigure(IntPtr data, IntPtr xdgSurface, uint serial)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
xdg_surface_ack_configure(xdgSurface, serial);
window._lastConfigureSerial = serial;
if (!window._configured)
{
window._configured = true;
if (window._pendingWidth > 0 && window._pendingHeight > 0)
{
window.ResizeBuffer(window._pendingWidth, window._pendingHeight);
}
window.Exposed?.Invoke(window, EventArgs.Empty);
}
}
private static void XdgToplevelConfigure(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
if (width > 0 && height > 0)
{
window._pendingWidth = width;
window._pendingHeight = height;
if (window._configured)
{
window.ResizeBuffer(width, height);
}
}
}
private static void XdgToplevelClose(IntPtr data, IntPtr toplevel)
{
var handle = GCHandle.FromIntPtr(data);
if (!handle.IsAllocated) return;
var window = (WaylandWindow)handle.Target!;
window.CloseRequested?.Invoke(window, EventArgs.Empty);
window._isRunning = false;
}
private static void BufferRelease(IntPtr data, IntPtr buffer)
{
// Buffer is available for reuse
}
#endregion
#region Public Methods
public void Show()
{
_isRunning = true;
// Attach buffer and commit
wl_surface_attach(_surface, _buffer, 0, 0);
wl_surface_damage_buffer(_surface, 0, 0, _width, _height);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
public void Hide()
{
wl_surface_attach(_surface, IntPtr.Zero, 0, 0);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
public void SetTitle(string title)
{
_title = title;
if (_xdgToplevel != IntPtr.Zero)
xdg_toplevel_set_title(_xdgToplevel, title);
}
public void Resize(int width, int height)
{
ResizeBuffer(width, height);
}
public void ProcessEvents()
{
if (!_isRunning || _display == IntPtr.Zero) return;
wl_display_dispatch_pending(_display);
wl_display_flush(_display);
}
public void Stop()
{
_isRunning = false;
}
public void CommitFrame()
{
if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero)
{
wl_surface_attach(_surface, _buffer, 0, 0);
wl_surface_damage_buffer(_surface, 0, 0, _width, _height);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
}
public int GetFileDescriptor()
{
return wl_display_get_fd(_display);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_isRunning = false;
if (_buffer != IntPtr.Zero)
{
wl_buffer_destroy(_buffer);
_buffer = IntPtr.Zero;
}
if (_shmPool != IntPtr.Zero)
{
wl_shm_pool_destroy(_shmPool);
_shmPool = IntPtr.Zero;
}
if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1))
{
munmap(_pixelData, (nuint)_bufferSize);
_pixelData = IntPtr.Zero;
}
if (_shmFd >= 0)
{
close(_shmFd);
_shmFd = -1;
}
if (_xdgToplevel != IntPtr.Zero)
{
xdg_toplevel_destroy(_xdgToplevel);
_xdgToplevel = IntPtr.Zero;
}
if (_xdgSurface != IntPtr.Zero)
{
xdg_surface_destroy(_xdgSurface);
_xdgSurface = IntPtr.Zero;
}
if (_surface != IntPtr.Zero)
{
wl_surface_destroy(_surface);
_surface = IntPtr.Zero;
}
if (_display != IntPtr.Zero)
{
wl_display_disconnect(_display);
_display = IntPtr.Zero;
}
// Free listener GCHandles
if (_registryListenerHandle.IsAllocated) _registryListenerHandle.Free();
if (_seatListenerHandle.IsAllocated) _seatListenerHandle.Free();
if (_pointerListenerHandle.IsAllocated) _pointerListenerHandle.Free();
if (_keyboardListenerHandle.IsAllocated) _keyboardListenerHandle.Free();
if (_wmBaseListenerHandle.IsAllocated) _wmBaseListenerHandle.Free();
if (_xdgSurfaceListenerHandle.IsAllocated) _xdgSurfaceListenerHandle.Free();
if (_toplevelListenerHandle.IsAllocated) _toplevelListenerHandle.Free();
if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free();
if (_thisHandle.IsAllocated)
_thisHandle.Free();
GC.SuppressFinalize(this);
}
~WaylandWindow()
{
Dispose();
}
#endregion
}