-
Notifications
You must be signed in to change notification settings - Fork 452
Add support for deferred SDL3 clipboard callbacks (and use it for images) #6254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
15d7d54
296f274
097b2ec
97f0bfe
23f760c
2eef0b0
06e8845
09e0466
20d3005
418c023
16bf604
bbe54ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,42 @@ | ||
| // Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. | ||
| // See the LICENCE file in the repository root for full licence text. | ||
|
|
||
| using System; | ||
| using System.Buffers; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.InteropServices; | ||
| using System.Text; | ||
| using osu.Framework.Allocation; | ||
| using osu.Framework.Logging; | ||
| using SDL; | ||
| using SixLabors.ImageSharp; | ||
| using SixLabors.ImageSharp.Formats; | ||
|
|
||
| namespace osu.Framework.Platform.SDL | ||
| { | ||
| public class SDL3Clipboard : Clipboard | ||
| { | ||
| /// <summary> | ||
| /// Supported formats for decoding images from the clipboard. | ||
| /// </summary> | ||
| // It's possible for a format to not have a registered decoder, but all default formats will have one: | ||
| // https://github.com/SixLabors/ImageSharp/discussions/1353#discussioncomment-9142056 | ||
| private static IEnumerable<string> supportedImageMimeTypes => SixLabors.ImageSharp.Configuration.Default.ImageFormats.SelectMany(f => f.MimeTypes); | ||
|
|
||
| /// <summary> | ||
| /// Format used for encoding (saving) images to the clipboard. | ||
| /// </summary> | ||
| private readonly IImageFormat imageFormat; | ||
|
|
||
| public SDL3Clipboard(IImageFormat imageFormat) | ||
| { | ||
| this.imageFormat = imageFormat; | ||
| } | ||
|
|
||
| // SDL cannot differentiate between string.Empty and no text (eg. empty clipboard or an image) | ||
| // doesn't matter as text editors don't really allow copying empty strings. | ||
| // assume that empty text means no text. | ||
|
|
@@ -17,12 +46,176 @@ public class SDL3Clipboard : Clipboard | |
|
|
||
| public override Image<TPixel>? GetImage<TPixel>() | ||
| { | ||
| foreach (string mimeType in supportedImageMimeTypes) | ||
| { | ||
| if (tryGetData(mimeType, Image.Load<TPixel>, out var image)) | ||
| { | ||
| Logger.Log($"Decoded {mimeType} from clipboard."); | ||
| return image; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public override bool SetImage(Image image) | ||
| { | ||
| return false; | ||
| ReadOnlyMemory<byte> memory; | ||
|
|
||
| // we can't save the image in the callback as the caller owns the image and might dispose it from under us. | ||
|
|
||
| using (var stream = new MemoryStream()) | ||
| { | ||
| image.Save(stream, imageFormat); | ||
|
|
||
| // The buffer is allowed to escape the lifetime of the MemoryStream. | ||
| // https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream.getbuffer?view=net-8.0 | ||
| // "This method works when the memory stream is closed." | ||
| memory = new ReadOnlyMemory<byte>(stream.GetBuffer(), 0, (int)stream.Length); | ||
| } | ||
|
|
||
| return trySetData(imageFormat.DefaultMimeType, () => memory); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Decodes data from a native memory span. Return null or throw an exception if the data couldn't be decoded. | ||
| /// </summary> | ||
| /// <typeparam name="T">Type of decoded data.</typeparam> | ||
| private delegate T? SpanDecoder<out T>(ReadOnlySpan<byte> span); | ||
|
|
||
| private static unsafe bool tryGetData<T>(string mimeType, SpanDecoder<T> decoder, out T? data) | ||
| { | ||
| if (SDL3.SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE) | ||
| { | ||
| data = default; | ||
| return false; | ||
| } | ||
|
|
||
| UIntPtr nativeSize; | ||
| IntPtr pointer = SDL3.SDL_GetClipboardData(mimeType, &nativeSize); | ||
|
|
||
| if (pointer == IntPtr.Zero) | ||
| { | ||
| Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL3.SDL_GetError()}"); | ||
| data = default; | ||
| return false; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| var nativeMemory = new ReadOnlySpan<byte>((void*)pointer, (int)nativeSize); | ||
| data = decoder(nativeMemory); | ||
| return data != null; | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| Logger.Error(e, $"Failed to decode clipboard data for {mimeType}."); | ||
| data = default; | ||
| return false; | ||
| } | ||
| finally | ||
| { | ||
| SDL3.SDL_free(pointer); | ||
| } | ||
| } | ||
|
|
||
| private static unsafe bool trySetData(string mimeType, Func<ReadOnlyMemory<byte>> dataProvider) | ||
| { | ||
| var callbackContext = new ClipboardCallbackContext(mimeType, dataProvider); | ||
| var objectHandle = new ObjectHandle<ClipboardCallbackContext>(callbackContext, GCHandleType.Normal); | ||
|
|
||
| // TODO: support multiple mime types in a single callback | ||
| fixed (byte* ptr = Encoding.UTF8.GetBytes(mimeType + '\0')) | ||
| { | ||
| int ret = SDL3.SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1); | ||
|
|
||
| if (ret < 0) | ||
| { | ||
| objectHandle.Dispose(); | ||
| Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL3.SDL_GetError()}"); | ||
| } | ||
|
|
||
| return ret == 0; | ||
| } | ||
| } | ||
|
|
||
| [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] | ||
| private static unsafe IntPtr dataCallback(IntPtr userdata, byte* mimeType, UIntPtr* length) | ||
| { | ||
| var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata, true); | ||
|
|
||
| if (!objectHandle.GetTarget(out var context) || context.MimeType != SDL3.PtrToStringUTF8(mimeType)) | ||
| { | ||
| *length = 0; | ||
| return IntPtr.Zero; | ||
| } | ||
|
|
||
| context.EnsureDataValid(); | ||
| *length = context.DataLength; | ||
| return context.Address; | ||
| } | ||
|
|
||
| [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] | ||
| private static void cleanupCallback(IntPtr userdata) | ||
| { | ||
| var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata, true); | ||
|
|
||
| if (objectHandle.GetTarget(out var context)) | ||
| { | ||
| context.Dispose(); | ||
| objectHandle.Dispose(); | ||
| } | ||
|
||
| } | ||
|
|
||
| private class ClipboardCallbackContext : IDisposable | ||
| { | ||
| public readonly string MimeType; | ||
|
|
||
| /// <summary> | ||
| /// Provider of data suitable for the <see cref="MimeType"/>. | ||
| /// </summary> | ||
| /// <remarks>Called when another application requests that mime type from the OS clipboard.</remarks> | ||
| private Func<ReadOnlyMemory<byte>>? dataProvider; | ||
|
|
||
| private MemoryHandle memoryHandle; | ||
|
|
||
| /// <summary> | ||
| /// Address of the <see cref="ReadOnlyMemory{T}"/> returned by the <see cref="dataProvider"/>. | ||
| /// </summary> | ||
| /// <remarks>Pinned and suitable for passing to unmanaged code.</remarks> | ||
| public unsafe IntPtr Address => (IntPtr)memoryHandle.Pointer; | ||
|
|
||
| /// <summary> | ||
| /// Length of the <see cref="ReadOnlyMemory{T}"/> returned by the <see cref="dataProvider"/>. | ||
| /// </summary> | ||
| public UIntPtr DataLength; | ||
|
||
|
|
||
| public ClipboardCallbackContext(string mimeType, Func<ReadOnlyMemory<byte>> dataProvider) | ||
| { | ||
| MimeType = mimeType; | ||
| this.dataProvider = dataProvider; | ||
| } | ||
|
|
||
| public void EnsureDataValid() | ||
| { | ||
| if (dataProvider == null) | ||
| { | ||
| Debug.Assert(Address != IntPtr.Zero); | ||
| Debug.Assert(DataLength != 0); | ||
| return; | ||
| } | ||
|
|
||
| var data = dataProvider(); | ||
| dataProvider = null!; | ||
| DataLength = (UIntPtr)data.Length; | ||
| memoryHandle = data.Pin(); | ||
| } | ||
|
Comment on lines
+198
to
+211
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is pinned once and
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is to make use of deferred rendering of clipboard content. If no application requests the clipboard data, this code never runs and we don't need to serialise it to bytes. This isn't of much use in this PR as the image is immediately serialised. |
||
|
|
||
| public void Dispose() | ||
| { | ||
| memoryHandle.Dispose(); | ||
| DataLength = 0; | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're passing
truehere which reads as if we should be cleaning this up, however that's not being done and appears incorrect to do in this callback. Should this be changed tofalseto prevent incorrect use?