diff --git a/src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs deleted file mode 100644 index 3edc593ae5..0000000000 --- a/src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Input; -using Avalonia.Input.Platform; -using Avalonia.Logging; -using static Avalonia.X11.XLib; - -namespace Avalonia.X11.Selections.Clipboard -{ - internal sealed class X11ClipboardImpl : IOwnedClipboardImpl - { - private readonly AvaloniaX11Platform _platform; - private readonly X11Info _x11; - private IAsyncDataTransfer? _storedDataTransfer; - private readonly IntPtr _handle; - private TaskCompletionSource? _storeAtomTcs; - private readonly int _maximumPropertySize; - - public X11ClipboardImpl(AvaloniaX11Platform platform) - { - _platform = platform; - _x11 = platform.Info; - _handle = CreateEventWindow(platform, OnEvent); - - var extendedMaxRequestSize = XExtendedMaxRequestSize(_platform.Display); - var maxRequestSize = XMaxRequestSize(_platform.Display); - _maximumPropertySize = - (int)Math.Min(0x100000, (extendedMaxRequestSize == IntPtr.Zero - ? maxRequestSize - : extendedMaxRequestSize).ToInt64() - 0x100); - } - - private unsafe void OnEvent(ref XEvent ev) - { - if (ev.type == XEventName.SelectionClear) - { - // We night have already regained the clipboard ownership by the time a SelectionClear message arrives. - if (GetOwner() != _handle) - _storedDataTransfer = null; - - _storeAtomTcs?.TrySetResult(true); - } - - else if (ev.type == XEventName.SelectionRequest) - { - var sel = ev.SelectionRequestEvent; - var resp = new XEvent - { - SelectionEvent = - { - type = XEventName.SelectionNotify, - send_event = 1, - display = _x11.Display, - selection = sel.selection, - target = sel.target, - requestor = sel.requestor, - time = sel.time, - property = IntPtr.Zero - } - }; - if (sel.selection == _x11.Atoms.CLIPBOARD) - { - resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property); - } - - XSendEvent(_x11.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp); - } - - else if (ev.type == XEventName.SelectionNotify) - { - if (ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD_MANAGER && - ev.SelectionEvent.target == _x11.Atoms.SAVE_TARGETS && - _x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && - _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) - { - _storeAtomTcs?.TrySetResult(true); - } - } - - IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property) - { - if (target == _x11.Atoms.TARGETS) - { - var atoms = ConvertDataTransfer(_storedDataTransfer); - XChangeProperty(_x11.Display, window, property, - _x11.Atoms.ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); - return property; - } - else if (target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) - { - return property; - } - else if (DataFormatHelper.ToDataFormat(target, _x11.Atoms) is { } dataFormat) - { - if (_storedDataTransfer is null) - return IntPtr.Zero; - - // Our default bitmap format is image/png - if (dataFormat.Identifier is "image/png" && _storedDataTransfer.Contains(DataFormat.Bitmap)) - dataFormat = DataFormat.Bitmap; - - if (!_storedDataTransfer.Contains(dataFormat)) - return IntPtr.Zero; - - if (TryGetDataAsBytes(_storedDataTransfer, dataFormat, target) is not { } bytes) - return IntPtr.Zero; - - _ = SendDataToClientAsync(window, property, target, bytes); - return property; - } - else if (target == _x11.Atoms.MULTIPLE && _x11.Atoms.MULTIPLE != IntPtr.Zero) - { - XGetWindowProperty(_x11.Display, window, property, IntPtr.Zero, new IntPtr(0x7fffffff), false, - _x11.Atoms.ATOM_PAIR, out _, out var actualFormat, out var nitems, out _, out var prop); - if (nitems == IntPtr.Zero) - return IntPtr.Zero; - if (actualFormat == 32) - { - var data = (IntPtr*)prop.ToPointer(); - for (var c = 0; c < nitems.ToInt32(); c += 2) - { - var subTarget = data[c]; - var subProp = data[c + 1]; - var converted = WriteTargetToProperty(subTarget, window, subProp); - data[c + 1] = converted; - } - - XChangeProperty(_x11.Display, window, property, _x11.Atoms.ATOM_PAIR, 32, PropertyMode.Replace, - prop.ToPointer(), nitems.ToInt32()); - } - - XFree(prop); - - return property; - } - else - return IntPtr.Zero; - } - - } - - private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom) - { - if (DataFormat.Text.Equals(format)) - { - var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult(); - - return DataFormatHelper.TryGetStringEncoding(targetFormatAtom, _x11.Atoms) is { } encoding ? - encoding.GetBytes(text ?? string.Empty) : - null; - } - - if (DataFormat.Bitmap.Equals(format)) - { - if (dataTransfer.TryGetValueAsync(DataFormat.Bitmap).GetAwaiter().GetResult() is not { } bitmap) - return null; - - using var stream = new MemoryStream(); - bitmap.Save(stream); - - return stream.ToArray(); - } - - if (DataFormat.File.Equals(format)) - { - if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files) - return null; - - return UriListHelper.FileUriListToUtf8Bytes(files); - } - - if (format is DataFormat stringFormat) - { - return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? - Encoding.UTF8.GetBytes(stringValue) : - null; - } - - if (format is DataFormat bytesFormat) - return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult(); - - Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform) - ?.Log(this, "Unsupported data format {Format}", format); - - return null; - } - - private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data) - { - data.Position = 0; - using var events = new EventStreamWindow(_platform, window); - using var _ = data; - XSelectInput(_x11.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask)); - var size = new IntPtr(data.Length); - XChangeProperty(_x11.Display, window, property, _x11.Atoms.INCR, 32, PropertyMode.Replace, ref size, 1); - var buffer = ArrayPool.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); - while (true) - { - if (null == await events.WaitForEventAsync(x => - x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property - && x.PropertyEvent.state == 1, TimeSpan.FromMinutes(1))) - break; - var read = await data.ReadAsync(buffer, 0, buffer.Length); - if(read == 0) - break; - XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, buffer, read); - } - ArrayPool.Shared.Return(buffer); - - // Finish the transfer - XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0); - } - - private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) - { - if (bytes.Length < _maximumPropertySize) - { - XChangeProperty(_x11.Display, window, property, target, 8, - PropertyMode.Replace, - bytes, bytes.Length); - return Task.CompletedTask; - } - - return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes)); - } - - private IntPtr GetOwner() - => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD); - - private SelectionReadSession OpenReadSession() => ClipboardReadSessionFactory.CreateSession(_platform); - - private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) - { - var atoms = new List { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; - - if (dataTransfer is not null) - { - foreach (var format in dataTransfer.Formats) - { - foreach (var atom in DataFormatHelper.ToAtoms(format, _x11.Atoms)) - atoms.Add(atom); - } - } - - return atoms.ToArray(); - } - - private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer) - { - if (_x11.Atoms.CLIPBOARD_MANAGER == IntPtr.Zero || _x11.Atoms.SAVE_TARGETS == IntPtr.Zero) - return Task.CompletedTask; - - var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER); - if (clipboardManager == IntPtr.Zero) - return Task.CompletedTask; - - // Skip storing atoms if the data object contains any non-trivial formats - if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f))) - return Task.CompletedTask; - - return StoreTextCoreAsync(); - - async Task StoreTextCoreAsync() - { - // Skip storing atoms if the trivial formats are too big - var text = await dataTransfer.TryGetTextAsync(); - if (text is null || text.Length * 2 > 64 * 1024) - return; - - if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted) - _storeAtomTcs = new TaskCompletionSource(); - - var atoms = ConvertDataTransfer(dataTransfer); - XChangeProperty(_x11.Display, _handle, _x11.Atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, _x11.Atoms.ATOM, 32, - PropertyMode.Replace, atoms, atoms.Length); - XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS, - _x11.Atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, _handle, IntPtr.Zero); - await _storeAtomTcs.Task; - } - } - - public Task ClearAsync() - { - _storedDataTransfer = null; - XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, IntPtr.Zero, IntPtr.Zero); - return Task.CompletedTask; - } - - public async Task TryGetDataAsync() - { - var owner = GetOwner(); - if (owner == IntPtr.Zero) - return null; - - if (owner == _handle && _storedDataTransfer is { } storedDataTransfer) - return storedDataTransfer; - - // Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous. - var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false); - if (dataFormats.Length == 0) - return null; - - // Get the items while we're in an async method. This does not get values, except for DataFormat.File. - var reader = new ClipboardDataReader(_platform, textFormatAtoms, dataFormats, owner); - var items = await reader.CreateItemsAsync(); - return new ClipboardDataTransfer(reader, dataFormats, items); - } - - private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync() - { - using var session = OpenReadSession(); - - var formatAtoms = await session.SendFormatRequest(0) ?? []; - return DataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); - } - - public Task SetDataAsync(IAsyncDataTransfer dataTransfer) - { - _storedDataTransfer = dataTransfer; - XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); - return StoreAtomsInClipboardManager(dataTransfer); - } - - public Task IsCurrentOwnerAsync() - => Task.FromResult(GetOwner() == _handle); - } -} diff --git a/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs b/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs new file mode 100644 index 0000000000..61efa3c0d0 --- /dev/null +++ b/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.Selections.Clipboard; + +internal sealed class X11ClipboardImpl(AvaloniaX11Platform platform) + : SelectionDataProvider(platform, platform.Info.Atoms.CLIPBOARD), IOwnedClipboardImpl +{ + private TaskCompletionSource? _storeAtomTcs; + + protected override void OnEvent(ref XEvent ev) + { + if (ev.type == XEventName.SelectionClear) + { + // We night have already regained the clipboard ownership by the time a SelectionClear message arrives. + if (GetOwner() != Handle) + DataTransfer = null; + + _storeAtomTcs?.TrySetResult(); + } + + else if (ev.type == XEventName.SelectionNotify) + { + var atoms = Platform.Info.Atoms; + + if (ev.SelectionEvent.selection == atoms.CLIPBOARD_MANAGER && + ev.SelectionEvent.target == atoms.SAVE_TARGETS) + { + _storeAtomTcs?.TrySetResult(); + } + } + + else + base.OnEvent(ref ev); + } + + private SelectionReadSession OpenReadSession() + => ClipboardReadSessionFactory.CreateSession(Platform); + + private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer) + { + var atoms = Platform.Info.Atoms; + + var clipboardManager = XGetSelectionOwner(Platform.Display, atoms.CLIPBOARD_MANAGER); + if (clipboardManager == IntPtr.Zero) + return Task.CompletedTask; + + // Skip storing atoms if the data object contains any non-trivial formats + if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f))) + return Task.CompletedTask; + + return StoreTextCoreAsync(); + + async Task StoreTextCoreAsync() + { + // Skip storing atoms if the trivial formats are too big + var text = await dataTransfer.TryGetTextAsync(); + if (text is null || text.Length * 2 > 64 * 1024) + return; + + if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted) + _storeAtomTcs = new TaskCompletionSource(); + + var atomValues = ConvertDataTransfer(dataTransfer); + + XChangeProperty(Platform.Display, Handle, atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, atoms.ATOM, 32, + PropertyMode.Replace, atomValues, atomValues.Length); + + XConvertSelection(Platform.Display, atoms.CLIPBOARD_MANAGER, atoms.SAVE_TARGETS, + atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, Handle, 0); + + await _storeAtomTcs.Task; + } + } + + public Task ClearAsync() + { + DataTransfer = null; + SetOwner(0); + return Task.CompletedTask; + } + + public async Task TryGetDataAsync() + { + var owner = GetOwner(); + if (owner == 0) + return null; + + if (owner == Handle && DataTransfer is { } storedDataTransfer) + return storedDataTransfer; + + // Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous. + var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false); + if (dataFormats.Length == 0) + return null; + + // Get the items while we're in an async method. This does not get values, except for DataFormat.File. + var reader = new ClipboardDataReader(Platform, textFormatAtoms, dataFormats, owner); + var items = await reader.CreateItemsAsync(); + return new ClipboardDataTransfer(reader, dataFormats, items); + } + + private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync() + { + using var session = OpenReadSession(); + + var formatAtoms = await session.SendFormatRequest(0) ?? []; + return DataFormatHelper.ToDataFormats(formatAtoms, Platform.Info.Atoms); + } + + public Task SetDataAsync(IAsyncDataTransfer dataTransfer) + { + DataTransfer = dataTransfer; + SetOwner(Handle); + return StoreAtomsInClipboardManager(dataTransfer); + } + + public Task IsCurrentOwnerAsync() + => Task.FromResult(GetOwner() == Handle); +} diff --git a/src/Avalonia.X11/Selections/SelectionDataProvider.cs b/src/Avalonia.X11/Selections/SelectionDataProvider.cs new file mode 100644 index 0000000000..1d47d5b3bb --- /dev/null +++ b/src/Avalonia.X11/Selections/SelectionDataProvider.cs @@ -0,0 +1,278 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Logging; +using Avalonia.X11.Selections.Clipboard; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.Selections; + +/// +/// Provides an X11 selection (clipboard/drag-and-drop) via a dedicated window. +/// +internal abstract class SelectionDataProvider : IDisposable +{ + private readonly int _maximumPropertySize; + + protected AvaloniaX11Platform Platform { get; } + + protected IntPtr Selection { get; } + + protected IntPtr Handle { get; private set; } + + public IAsyncDataTransfer? DataTransfer { get; set; } + + protected SelectionDataProvider(AvaloniaX11Platform platform, IntPtr selection) + { + Platform = platform; + Selection = selection; + + var maxRequestSize = XExtendedMaxRequestSize(Platform.Display); + if (maxRequestSize == 0) + maxRequestSize = XMaxRequestSize(Platform.Display); + + _maximumPropertySize = (int)Math.Min(0x100000, maxRequestSize - 0x100); + + Handle = CreateEventWindow(platform, OnEvent); + } + + protected IntPtr GetOwner() + => XGetSelectionOwner(Platform.Display, Selection); + + protected void SetOwner(IntPtr owner) + => XSetSelectionOwner(Platform.Display, Selection, owner, 0); + + protected virtual void OnEvent(ref XEvent ev) + { + if (ev.type == XEventName.SelectionRequest) + { + var sel = ev.SelectionRequestEvent; + var resp = new XEvent + { + SelectionEvent = + { + type = XEventName.SelectionNotify, + send_event = 1, + display = Platform.Display, + selection = sel.selection, + target = sel.target, + requestor = sel.requestor, + time = sel.time, + property = 0 + } + }; + + if (sel.selection == Selection) + { + resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property); + } + + XSendEvent(Platform.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp); + } + + IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property) + { + var atoms = Platform.Info.Atoms; + + if (target == atoms.TARGETS) + { + var atomValues = ConvertDataTransfer(DataTransfer); + XChangeProperty(Platform.Display, window, property, atoms.ATOM, 32, PropertyMode.Replace, + atomValues, atomValues.Length); + return property; + } + + if (target == atoms.SAVE_TARGETS) + { + return property; + } + + if (DataFormatHelper.ToDataFormat(target, atoms) is { } dataFormat) + { + if (DataTransfer is null) + return 0; + + // Our default bitmap format is image/png + if (dataFormat.Identifier is "image/png" && DataTransfer.Contains(DataFormat.Bitmap)) + dataFormat = DataFormat.Bitmap; + + if (!DataTransfer.Contains(dataFormat)) + return 0; + + if (TryGetDataAsBytes(DataTransfer, dataFormat, target) is not { } bytes) + return 0; + + _ = SendDataToClientAsync(window, property, target, bytes); + return property; + } + + if (target == atoms.MULTIPLE) + { + XGetWindowProperty(Platform.Display, window, property, 0, int.MaxValue, false, + atoms.ATOM_PAIR, out _, out var actualFormat, out var nitems, out _, out var prop); + + if (nitems == 0) + return 0; + + if (actualFormat == 32) + { + unsafe + { + var data = (IntPtr*)prop.ToPointer(); + for (var c = 0; c < nitems.ToInt32(); c += 2) + { + var subTarget = data[c]; + var subProp = data[c + 1]; + var converted = WriteTargetToProperty(subTarget, window, subProp); + data[c + 1] = converted; + } + + XChangeProperty(Platform.Display, window, property, atoms.ATOM_PAIR, 32, PropertyMode.Replace, + prop.ToPointer(), nitems.ToInt32()); + } + } + + XFree(prop); + + return property; + } + + return 0; + } + + } + + private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom) + { + if (DataFormat.Text.Equals(format)) + { + var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult(); + + return DataFormatHelper.TryGetStringEncoding(targetFormatAtom, Platform.Info.Atoms) is { } encoding ? + encoding.GetBytes(text ?? string.Empty) : + null; + } + + if (DataFormat.Bitmap.Equals(format)) + { + if (dataTransfer.TryGetValueAsync(DataFormat.Bitmap).GetAwaiter().GetResult() is not { } bitmap) + return null; + + using var stream = new MemoryStream(); + bitmap.Save(stream); + + return stream.ToArray(); + } + + if (DataFormat.File.Equals(format)) + { + if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files) + return null; + + return UriListHelper.FileUriListToUtf8Bytes(files); + } + + if (format is DataFormat stringFormat) + { + return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? + Encoding.UTF8.GetBytes(stringValue) : + null; + } + + if (format is DataFormat bytesFormat) + return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult(); + + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform) + ?.Log(this, "Unsupported data format {Format}", format); + + return null; + } + + private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data) + { + data.Position = 0; + + using var events = new EventStreamWindow(Platform, window); + using var _ = data; + + XSelectInput(Platform.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask)); + + var size = new IntPtr(data.Length); + XChangeProperty(Platform.Display, window, property, Platform.Info.Atoms.INCR, 32, PropertyMode.Replace, + ref size, 1); + + var buffer = ArrayPool.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); + + while (true) + { + var evt = await events.WaitForEventAsync( + x => + x.type == XEventName.PropertyNotify && + x.PropertyEvent.atom == property && + x.PropertyEvent.state == 1, + TimeSpan.FromMinutes(1)); + + if (evt is null) + break; + + var read = await data.ReadAsync(buffer, 0, buffer.Length); + if (read == 0) + break; + + XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, buffer, read); + } + + ArrayPool.Shared.Return(buffer); + + // Finish the transfer + XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, 0, 0); + } + + private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) + { + if (bytes.Length < _maximumPropertySize) + { + XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, + bytes, bytes.Length); + return Task.CompletedTask; + } + + return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes)); + } + + protected IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) + { + var atoms = Platform.Info.Atoms; + var atomValues = new List { atoms.TARGETS, atoms.MULTIPLE }; + + if (dataTransfer is not null) + { + foreach (var format in dataTransfer.Formats) + { + foreach (var atom in DataFormatHelper.ToAtoms(format, atoms)) + atomValues.Add(atom); + } + } + + return atomValues.ToArray(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Handle == 0) + return; + + XDestroyWindow(Platform.Display, Handle); + Handle = 0; + } +}