Skip to content

Conversation

MrJul
Copy link
Member

@MrJul MrJul commented Jul 29, 2025

What does the pull request do?

This PR:

  • Provides a new public API for clipboard and drag-and-drop (DnD), shared as much as possible between the two.
  • Refactors how the clipboard is implemented internally for the various platforms.

The aim of this PR isn't to implement all missing clipboard features, but rather to pose a solid foundation so those can be easily implemented later on top of it (such as bitmap support and data sharing).

New API

DataFormat

A new DataFormat class is introduced. Each equatable instance represents a single data format, for both the clipboard and DnD. The purpose of DataFormat is double:

  • First, to avoid having magic strings being passed around, especially for cross-platform clipboard formats (Text and Files), which are Avalonia concepts that shouldn't really be exposed directly as strings.
  • Second, to avoid clashes between existing platform format names and user-defined ones, causing confusion and potentially unexpected crashes.

There are three kinds of DataFormat:

  • Universal: a cross-platform format, completely handled by Avalonia and automatically converted to the appropriate representation(s) for each platform. It can't be defined by the user. Two currently exist: DataFormat.Text and DataFormat.File. (DataFormat.Image will be implemented in a later PR).
  • Application: an user defined format, created using DataFormat.CreateApplicationFormat(string identifier). The identifier will be internally prefixed – with the exact format name determined by the platform – to avoid clashes with predefined operating system formats.
  • Platform: a platform system format, created using DataFormat.CreateOperatingSystemFormat(string identifier). The identifier will be passed AS IS to the underlying operating system. This is the direct equivalent to the strings used previously, but without ambiguity. When using such a format, the user must ensure that the data is what the current platform expects. No automatic conversion takes place.

Application and OperatingSystem support the following data types when being written to the clipboard: string, byte[], Memory<byte> and Stream. Other types will result in a warning and won't be placed onto the clipboard. Reading returns either string or byte[].

IDataTransfer & IDataTransferItem

Guided by #12600, the main goal of this PR was to implement everything on top of IDataObject (or an equivalent), so the logic could be shared between the clipboard and DnD. There were several discussions internally around having IAsyncDataObject (mostly for the clipboard) and IDataObject (mostly for DnD, which is synchronous by nature on almost all platforms). The first version of the refactor implemented both. After spending days on it, I wasn't satisfied with the result at all. Code was either duplicated everywhere, or conversions happening so often that the distinction between IDataObject and IAsyncDataObject was becoming meaningless.

Edit 2025-08-08: due to concerns around async usages in synchronous drag handlers, this has been split back into synchronous and asynchronous types. While this duplicates the public API, it's now way better represented internally compared to my first attempt thanks to the knowledge I gained while working on this feature.

Moreover, after gluing those two classes together in a second attempt, I quickly realized that the existing IDataObject shape was hindering the design. While good on paper, it has a critical flaw: there's only one value available per format, which is good enough for Windows and X11 — they exhibit the same restriction – but certainly not for other platforms. As mentioned in #12600, macOS allows several instances of NSPasteboardItem to coexist on the clipboard. iOS clipboard has the same capability. Android, while a bit more limited, also allows severeal ClipData.Item. Last but not least, the recent-ish ClipboardItem can exist in multiple copies in the browser's clipboard.

Additionally, macOS requires multiple items support for DnD to work properly with files on modern macOS versions. While this could of course be hacked around, it didn't feel quite right.

At that point, it made complete sense to ditch the one-value-per-format paradigm and to shift towards have multiple items instead. Enter IDataTransfer and IDataTransferItem and their async equivalent IAsyncDataTransfer and IAsyncDataTransferItem:

public interface IDataTransfer : IDisposable
{
    IReadOnlyList<DataFormat> Formats { get; }
    IReadOnlyList<IDataTransferItem> Items { get; }
}

public interface IDataTransferItem
{
    IReadOnlyList<DataFormat> Formats { get; }
    object? TryGet(DataFormat format);
}

public interface IAsyncDataTransfer : IDisposable
{
    IReadOnlyList<DataFormat> Formats { get; }
    IReadOnlyList<IAsyncDataTransferItem> Items { get; }
}

public interface IAsyncDataTransferItem
{
    IReadOnlyList<DataFormat> Formats { get; }
    Task<object?> TryGetAsync(DataFormat format);
}

These two interfaces went through several iterations. There were versions where Formats and/or Items were either async methods or iterators. However, after implementing all platforms, it turns out that almost all of them provide the formats synchronously and eagerly (after getting a hold of the clipboard's contents): macOS, iOS, Android and the Web. Windows provides a synchronous but lazy enumeration, which was getting cached anyways... As such, exposing the format as a list felt natural. The same logic applies to the items, which are all directly exposed as a list for the four platforms supporting them.

The important point is that for a given format, the value can be retrieved on demand and asynchronously through TryGetAsync (if the underlying platform supports it, of course).

These interfaces are kept very simple on purpose, allowing them to be easily implemented by each platform.

Extension methods for IDataTransfer and IAsyncDataTransfer

Users of Avalonia might find the previous interfaces a bit too generic to use comfortably. This PR adds several extension methods over IDataTransfer and IDataTransferItem for convenience:

// Filtering by format
public static bool Contains(this IDataTransfer dataTransfer, DataFormat format);
public static bool Contains(this IDataTransferItem dataTransferItem, DataFormat format);
public static IEnumerable<IDataTransferItem> GetItems(this IDataTransfer dataTransfer, DataFormat format);

// Retrieving values corresponding to a given format
public static T? TryGetValue<T>(this IDataTransfer dataTransfer, DataFormat format);
public static T[]? TryGetValues<T>(this IDataTransfer dataTransfer, DataFormat format);

// Retrieving values for cross-platform formats
public static string? TryGetText(this IDataTransfer dataTransfer);
public static IStorageItem? TryGetFile(this IDataTransfer dataTransfer);
public static IStorageItem[]? TryGetFiles(this IDataTransfer dataTransfer);

Asynchronous counterparts of all the TryGetX methods above exist for IAsyncDataTransfer.

IClipboard & IClipboardImpl

IClipboard has been rewritten in terms of IAsyncDataTransfer. Here is its new shape (with obsolete methods excluded):

[NotClientImplementable]
public interface IClipboard
{
    Task ClearAsync();
    Task SetDataAsync(IAsyncDataTransfer? dataTransfer);
    Task<IAsyncDataTransfer?> TryGetDataAsync();
    Task<IAsyncDataTransfer?> TryGetInProcessDataAsync();
    Task FlushAsync();
}

Notice that there isn't a way to retrieve the value of a specific format anymore. This capability is already provided by IAsyncDataTransfer and thus isn't duplicated by IClipboard. The API is also now symmetric: gone are the days of using IDataObject only while writing.

IClipboard now has a single implementation, cleverly named Clipboard. It provides common clipboard logic and delegates the specifics to a similar interface platforms need to implement, IClipboardImpl:

[PrivateApi]
public interface IClipboardImpl
{
    Task<IAsyncDataTransfer?> TryGetDataAsync();
    Task SetDataAsync(IAsyncDataTransfer dataTransfer);
    Task ClearAsync();
}

Optionally, platforms can provide the ability for the clipboard to be flushed, or to query whether it's been externally changed:

[PrivateApi]
public interface IFlushableClipboardImpl : IClipboardImpl
{
    Task FlushAsync();
}

[PrivateApi]
public interface IOwnedClipboardImpl : IClipboardImpl
{
    Task<bool> IsCurrentOwnerAsync();
}

Extension methods for IClipboard

In the same fashion as IDataTransfer, several extension methods to IClipboard are provided for convenience, simplifying the code when a single format needs to be read or written.

If the clipboard is to be accessed several times in a row, it is recommended to use TryGetDataAsync() and the extension methods over the returned IAsyncDataTransfer instead.

// Formats
public static async Task<IReadOnlyList<DataFormat>> GetDataFormatsAsync(this IClipboard clipboard);

// Read
public static async Task<T?> TryGetValueAsync<T>(this IClipboard clipboard, DataFormat format);
public static async Task<T[]?> TryGetValuesAsync<T>(this IClipboard clipboard, DataFormat format);
public static Task<string?> TryGetTextAsync(this IClipboard clipboard);
public static Task<IStorageItem?> TryGetFileAsync(this IClipboard clipboard);
public static Task<IStorageItem[]?> TryGetFilesAsync(this IClipboard clipboard);

// Write
public static Task SetValueAsync<T>(this IClipboard clipboard, DataFormat format, T? value);
public static Task SetValuesAsync<T>(this IClipboard clipboard, DataFormat format, IEnumerable<T>? values);
public static Task SetTextAsync(this IClipboard clipboard, string? text);
public static Task SetFileAsync(this IClipboard clipboard, IStorageItem? file);
public static Task SetFilesAsync(this IClipboard clipboard, IEnumerable<IStorageItem>? files);

IAsyncDataTransfer lifetime management

The returned IAsyncDataTransfer might hold onto native platform resources; consequently, it needs to be properly disposed.

IClipboard adopts a simple scheme:

  • Any IAsyncDataTransfer instance returned by IClipboard.TryGetDataAsync() needs to be disposed by its caller as soon as possible (ownership is transferred to the caller).
  • Any IAsyncDataTransfer instance passed to IClipboard.SetDataAsync() must not be disposed while it's on the clipboard. Avalonia take cares of disposing it when necessary (ownership is transferred to the clipboard).

The extension methods for IClipboard take care of that automatically.

Platform improvements

  • Windows: the clipboard code has been reorganized.
  • macOS: the native clipboard code has been completely rewritten: data is now provided on-demand and as multiple items.
  • macOS: dragging and dropping files to the Finder works.
  • X11: the File format is now handled, allowing copying and pasting files. IStorageItem instances gets serialized as an URI list of type text/uri-list, which your favorite file manager probably supports.
  • iOS: any supported format can be read and written (instead of just text before).
  • Android: IStorageItem, Uri and Intent objects can now be read and written (instead of just text before). By implementing a ContentProvider, users can now share arbitrary data.
  • Browser: any supported format can be read and written (instead of just text before).

Other notes

As mentioned earlier, reading binary data from an IDataTransferItem object will return a byte[]. While we could sometimes return a Stream directly to the underlying data without an extra copy, managing its lifetime can be problematic, especially when object is returned. While that problem is definitely solvable, the current PR already contains many changes, and adding stream support can be done in a later PR.

Reviewing this PR is probably best done project by project, starting with Avalonia.Base. I've tried to squash my numerous WIP commits into platform specific ones, but there are still changes from later commits that are impacting the overall API shape.

XML documentation has been added to all new public types and members.

Breaking changes

Normally none for public API.

While a bunch of members have been obsoleted (see below), the binary breaking changes are only in [PrivateApi] types, which aren't meant to be implemented.

Obsoletions / Deprecations

IDataObject and all members using it are now obsolete. It should still work as expected, with string formats being converted to operating system DataFormat automatically internally.

IClipboard methods for specific formats (such as GetTextAsync()) have been replaced by extension methods.

Fixed issues

@MrJul MrJul added feature needs-api-review The PR adds new public APIs that should be reviewed. labels Jul 29, 2025
# Conflicts:
#	src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@869570967
Copy link

Can you support the Clipboard.SetImage() and Clipboard.GetImage() methods?

@MrJul
Copy link
Member Author

MrJul commented Aug 7, 2025

Can you support the Clipboard.SetImage() and Clipboard.GetImage() methods?

These are planned in a subsequent PR.

As mentioned in the description:

The aim of this PR isn't to implement all missing clipboard features, but rather to pose a solid foundation so those can be easily implemented later on top of it (such as bitmap support and data sharing).

@MrJul
Copy link
Member Author

MrJul commented Aug 7, 2025

After feedback from @kekekeks, IDataTransfer and IDataTransferItem are now synchronous and IAsyncDataTransfer and IAsyncDataTransferItem have been introduced as well.

The reasoning is that drag and drop operations are inherently synchronous and providing an API that is only asynchronous causes several problems: mandatory async void handlers (which are very easy to get wrong), and sync-over-async code without any guarantee about how the operations get dispatched.

While this duplicates the public API, the internal implementations are more robust. Compared to my first attempt, where the sync/async boundaries were unclear, it's much better now thanks to the knowledge I've gained since then.

The PR description has been updated.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0058114-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul mentioned this pull request Aug 8, 2025
3 tasks
/// <param name="dataTransfer">The <see cref="IDataTransfer"/> instance.</param>
/// <param name="format">The format to retrieve.</param>
/// <returns>A list of values for <paramref name="format"/>, or null if the format is not supported.</returns>
public static T[]? TryGetValues<T>(this IDataTransfer dataTransfer, DataFormat format)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit worrying users might consider this API supporting (de)serialization, since we allow generics here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not new, we allowed arbitrary objects before too. It makes it easier for the in-process dnd, but less obvious for inter-process dnd and clipboard. We cannot reliably support (de)serialization.
I don't have a better ideas for the API here though.

My best thought before (for the old API) was following:

Task<string> GetTextAsync();
Task<IStorageFile> GetFileAsync();
Task<byte[]> GetByteArray(string format);

Without open generic overloads, and with corresponding set methods. Potentially with Set/GetInProcessObject<T>().
But this obviously would affect existing applications too much.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point I had a generic version of DataFormat, DataFormat<T> where you could supply a type and a (de)serializer. While it worked fine when putting objects in the clipboard, retrieving them was a bit messy: the user had to ensure that known formats were registered to provide the correct type, which could vary from call to call. And unknown formats were left out as byte[] anyway.

I'm not opposed to removing this and keep only overloads returning a byte[] or other similar binary types (Stream) when there's a DataFormat argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea: what about exposing a wrapper type, e.g. DataTransferValue that only accepts byte[]/Memory<byte>/string/Stream (it could have implicit conversions)?

However... Thinking about it a little more, we can also have platform specific types.
For example, with this PR, the Android clipboard supports setting System.Uri, Android.Net.Uri and Android.Content.Intent. These types are important because they're the only way to support advanced clipboard scenarios on Android.

If we don't expose generic or object overloads, we're completely closing this possibility.

I'm a bit torn between being stricter in the types we accept, or leave them open and simply document this fact.

Comment on lines +10 to +13
// TODO: this name isn't ideal. For instance, it's not a valid UTI for macOS.
// We currently have a converter in place in native code for backwards compatibility with IDataObject,
// but this should ideally be removed at some point.
// Consider using DataFormat.CreateApplicationFormat() instead (breaking change).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, CreateApplicationFormat won't work here.
This data format is supposed to be "operating system format" one, and shared between devtools (old and new - both use the same data format) and AvaloniaVS extension.
See ElementsNavView.axaml.cs#L59 in new devtools and AvaloniaVS one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's primary for the Visual Studio extension, macOS is not so big of a problem. Unless Rider will support the same format

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to have the format work on all platforms, it should not be an "operating system format" since the name would need to be different between OSes (macOS must use an UTI). By using an application format, we can tell Avalonia to generate an appropriate but deterministic name here.

However, the right thing to do here if we consider this format public is probably to have proper documented names per OS for it. Then Rider or other extensions are free to use them.

AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Clipboard type is the same on all platforms, do we need to register it via locator? As an alternative, it can be created in the application, when requested.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inded, we can change Clipboard to not be registered indeed.
I'm a bit worried about breaking testability by doing so though.

@maxkatz6
Copy link
Member

Not related to this PR directly, but winforms got new clipboard API - see dotnet/winforms#12362
And same API will likely get merged into WPF eventually.
Aside from addition of JSON overloads, we need to be sure it's still compatible for XPF.

@maxkatz6
Copy link
Member

I did review shared/core part, and a little of platform specific code. Will need another pass for the rest of platform code.

@MrJul
Copy link
Member Author

MrJul commented Aug 19, 2025

Full API diff (updated 2025-09-15):

Avalonia.Base (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Input
  {
      public static class DragDrop
      {
+         public static Task<DragDropEffects> DoDragDropAsync(PointerEventArgs triggerEvent, IDataTransfer dataTransfer, DragDropEffects allowedEffects);
      }
      public class DragEventArgs : RoutedEventArgs
      {
+         public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataTransfer dataTransfer, Interactive target, Point targetLocation, KeyModifiers keyModifiers);
+         public IDataTransfer DataTransfer { get; }
      }
+     public static class AsyncDataTransferExtensions
+     {
+         public static bool Contains(this IAsyncDataTransfer dataTransfer, DataFormat format);
+         public static IEnumerable<IAsyncDataTransferItem> GetItems(this IAsyncDataTransfer dataTransfer, DataFormat format);
+         public static Task<IStorageItem?> TryGetFileAsync(this IAsyncDataTransfer dataTransfer);
+         public static Task<IStorageItem[]?> TryGetFilesAsync(this IAsyncDataTransfer dataTransfer);
+         public static Task<string?> TryGetTextAsync(this IAsyncDataTransfer dataTransfer);
+         public static Task<T?> TryGetValueAsync<T>(this IAsyncDataTransfer dataTransfer, DataFormat format);
+         public static Task<T[]?> TryGetValuesAsync<T>(this IAsyncDataTransfer dataTransfer, DataFormat format);
+     }
+     public static class AsyncDataTransferItemExtensions
+     {
+         public static bool Contains(this IAsyncDataTransferItem dataTransferItem, DataFormat format);
+     }
+     public sealed record DataFormat
+     {
+         public static DataFormat CreateApplicationFormat(string identifier);
+         public static DataFormat CreatePlatformFormat(string identifier);
+         public static DataFormat FromSystemName(string systemName, string applicationPrefix);
+         public override string ToString();
+         public string ToSystemName(string applicationPrefix);
+         public static DataFormat File { get; }
+         public string Identifier { get; }
+         public DataFormatKind Kind { get; }
+         public static DataFormat Text { get; }
+     }
+     public enum DataFormatKind
+     {
+         Application = 0,
+         Platform = 1,
+         Universal = 2,
+     }
+     public sealed class DataTransfer : IDataTransfer, IAsyncDataTransfer
+     {
+         public DataTransfer();
+         public void Add(DataTransferItem item);
+         public IReadOnlyList<DataFormat> Formats { get; }
+         public IReadOnlyList<DataTransferItem> Items { get; }
+     }
+     public static class DataTransferExtensions
+     {
+         public static bool Contains(this IDataTransfer dataTransfer, DataFormat format);
+         public static IEnumerable<IDataTransferItem> GetItems(this IDataTransfer dataTransfer, DataFormat format);
+         public static IStorageItem? TryGetFile(this IDataTransfer dataTransfer);
+         public static IStorageItem[]? TryGetFiles(this IDataTransfer dataTransfer);
+         public static string? TryGetText(this IDataTransfer dataTransfer);
+         public static T? TryGetValue<T>(this IDataTransfer dataTransfer, DataFormat format);
+         public static T[]? TryGetValues<T>(this IDataTransfer dataTransfer, DataFormat format);
+     }
+     public sealed class DataTransferItem : IDataTransferItem, IAsyncDataTransferItem
+     {
+         public DataTransferItem();
+         public static DataTransferItem Create<T>(DataFormat format, T value);
+         public static DataTransferItem Create<T>(DataFormat format, Func<T> getValue);
+         public static DataTransferItem CreateFile(IStorageItem? value);
+         public static DataTransferItem CreateText(string? value);
+         public void Set<T>(DataFormat format, T value);
+         public void Set<T>(DataFormat format, System.Func<T> getValue);
+         public void SetFile(IStorageItem? value);
+         public void SetText(string? value);
+         public object? TryGet(DataFormat format);
+         public IReadOnlyList<DataFormat> Formats { get; }
+     }
+     public static class DataTransferItemExtensions
+     {
+         public static bool Contains(this IDataTransferItem dataTransferItem, DataFormat format);
+     }
+     public interface IAsyncDataTransfer
+     {
+         IReadOnlyList<DataFormat> Formats { get; }
+         IReadOnlyList<IAsyncDataTransferItem> Items { get; }
+     }
+     public interface IAsyncDataTransferItem
+     {
+         Task<object?> TryGetAsync(DataFormat format);
+         IReadOnlyList<DataFormat> Formats { get; }
+     }
+     public interface IDataTransfer
+     {
+         IReadOnlyList<DataFormat> Formats { get; }
+         IReadOnlyList<IDataTransferItem> Items { get; }
+     }
+     public interface IDataTransferItem
+     {
+         object? TryGet(DataFormat format);
+         IReadOnlyList<DataFormat> Formats { get; }
+     }
  }
  namespace Platform
  {
      public interface IClipboard
      {
+         Task SetDataAsync(IAsyncDataTransfer? dataTransfer);
+         Task<IAsyncDataTransfer?> TryGetDataAsync();
+         Task<IAsyncDataTransfer?> TryGetInProcessDataAsync();
      }
      public interface IPlatformDragSource
      {
+         Task<DragDropEffects> DoDragDropAsync(PointerEventArgs triggerEvent, IDataTransfer dataTransfer, DragDropEffects allowedEffects);
      }
+     public static class ClipboardExtensions
+     {
+         public static Task<IReadOnlyList<DataFormat>> GetDataFormatsAsync(this IClipboard clipboard);
+         public static Task SetFileAsync(this IClipboard clipboard, IStorageItem? file);
+         public static Task SetFilesAsync(this IClipboard clipboard, IEnumerable<IStorageItem>? files);
+         public static Task SetTextAsync(this IClipboard clipboard, string? text);
+         public static Task SetValueAsync<T>(this IClipboard clipboard, DataFormat format, T? value);
+         public static Task SetValuesAsync<T>(this IClipboard clipboard, DataFormat format, IEnumerable<T>? values);
+         public static Task<IStorageItem?> TryGetFileAsync(this IClipboard clipboard);
+         public static Task<IStorageItem[]?> TryGetFilesAsync(this IClipboard clipboard);
+         public static Task<string?> TryGetTextAsync(this IClipboard clipboard);
+         public static Task<T?> TryGetValueAsync<T>(this IClipboard clipboard, DataFormat format);
+         public static Task<T[]?> TryGetValuesAsync<T>(this IClipboard clipboard, DataFormat format);
+     }
+     public interface IClipboardImpl
+     {
+         Task ClearAsync();
+         Task SetDataAsync(IAsyncDataTransfer dataTransfer);
+         Task<IAsyncDataTransfer?> TryGetDataAsync();
+     }
+     public interface IFlushableClipboardImpl : IClipboardImpl
+     {
+         Task FlushAsync();
+     }
+     public interface IOwnedClipboardImpl : IClipboardImpl
+     {
+         Task<bool> IsCurrentOwnerAsync();
+     }
  }
  namespace Raw
  {
      public class RawDragEvent : RawInputEventArgs
      {
+         public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, IInputRoot root, Avalonia.Point location, IDataTransfer dataTransfer, DragDropEffects effects, RawInputModifiers modifiers);
+         public IDataTransfer DataTransfer { get; }
      }
  }

Avalonia.Diagnostics (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Diagnostics
  {
+     public static class DevToolsDataFormats
+     {
+         public static DataFormat Selector { get; }
+     }
  }

Avalonia.Headless (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Headless
  {
      public static class HeadlessWindowExtensions
      {
+         public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataTransfer data, DragDropEffects effects, RawInputModifiers modifiers = 0);
      }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature needs-api-review The PR adds new public APIs that should be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Clipboard / DnD changes Drag/Drop issue from Avalonia App to the Finder of macOS
4 participants