// 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; /// /// Native Wayland window implementation using xdg-shell protocol. /// Provides full Wayland support without XWayland dependency. /// 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? KeyDown; public event EventHandler? KeyUp; public event EventHandler? TextInput; public event EventHandler? PointerMoved; public event EventHandler? PointerPressed; public event EventHandler? PointerReleased; public event EventHandler? 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(RegistryGlobal), GlobalRemove = Marshal.GetFunctionPointerForDelegate(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(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(XdgToplevelConfigure), Close = Marshal.GetFunctionPointerForDelegate(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(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(SeatCapabilities), Name = Marshal.GetFunctionPointerForDelegate(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(PointerEnter), Leave = Marshal.GetFunctionPointerForDelegate(PointerLeave), Motion = Marshal.GetFunctionPointerForDelegate(PointerMotion), Button = Marshal.GetFunctionPointerForDelegate(OnPointerButton), Axis = Marshal.GetFunctionPointerForDelegate(PointerAxis), Frame = Marshal.GetFunctionPointerForDelegate(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(KeyboardKeymap), Enter = Marshal.GetFunctionPointerForDelegate(KeyboardEnter), Leave = Marshal.GetFunctionPointerForDelegate(KeyboardLeave), Key = Marshal.GetFunctionPointerForDelegate(KeyboardKey), Modifiers = Marshal.GetFunctionPointerForDelegate(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(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 }