diff --git a/osu.Framework/Allocation/ObjectHandle.cs b/osu.Framework/Allocation/ObjectHandle.cs index 26b6758c2c..c66b02edc3 100644 --- a/osu.Framework/Allocation/ObjectHandle.cs +++ b/osu.Framework/Allocation/ObjectHandle.cs @@ -29,7 +29,7 @@ public struct ObjectHandle : IDisposable private GCHandle handle; - private readonly bool fromPointer; + private readonly bool canFree; /// /// Wraps the provided object with a , using the given . @@ -39,18 +39,19 @@ public struct ObjectHandle : IDisposable public ObjectHandle(T target, GCHandleType handleType) { handle = GCHandle.Alloc(target, handleType); - fromPointer = false; + canFree = true; } /// /// Recreates an based on the passed . - /// Disposing this object will not free the handle, the original object must be disposed instead. + /// If is true, disposing this object will free the handle. /// - /// Handle. - public ObjectHandle(IntPtr handle) + /// from a previously constructed . + /// Whether this instance owns the underlying . + public ObjectHandle(IntPtr handle, bool ownsHandle = false) { this.handle = GCHandle.FromIntPtr(handle); - fromPointer = true; + canFree = ownsHandle; } /// @@ -87,7 +88,7 @@ public bool GetTarget(out T target) public void Dispose() { - if (!fromPointer && handle.IsAllocated) + if (canFree && handle.IsAllocated) handle.Free(); } diff --git a/osu.Framework/Platform/SDL/SDL3Clipboard.cs b/osu.Framework/Platform/SDL/SDL3Clipboard.cs index e7eea7a816..786da27fd5 100644 --- a/osu.Framework/Platform/SDL/SDL3Clipboard.cs +++ b/osu.Framework/Platform/SDL/SDL3Clipboard.cs @@ -1,13 +1,42 @@ // Copyright (c) ppy Pty Ltd . 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 { + /// + /// Supported formats for decoding images from the clipboard. + /// + // 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 supportedImageMimeTypes => SixLabors.ImageSharp.Configuration.Default.ImageFormats.SelectMany(f => f.MimeTypes); + + /// + /// Format used for encoding (saving) images to the clipboard. + /// + 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,175 @@ public class SDL3Clipboard : Clipboard public override Image? GetImage() { + foreach (string mimeType in supportedImageMimeTypes) + { + if (tryGetData(mimeType, Image.Load, out var image)) + { + Logger.Log($"Decoded {mimeType} from clipboard."); + return image; + } + } + return null; } public override bool SetImage(Image image) { - return false; + ReadOnlyMemory 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(stream.GetBuffer(), 0, (int)stream.Length); + } + + return trySetData(imageFormat.DefaultMimeType, () => memory); + } + + /// + /// Decodes data from a native memory span. Return null or throw an exception if the data couldn't be decoded. + /// + /// Type of decoded data. + private delegate T? SpanDecoder(ReadOnlySpan span); + + private static unsafe bool tryGetData(string mimeType, SpanDecoder 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((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> dataProvider) + { + var callbackContext = new ClipboardCallbackContext(mimeType, dataProvider); + var objectHandle = new ObjectHandle(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) + { + using var objectHandle = new ObjectHandle(userdata); + + 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) + { + using var objectHandle = new ObjectHandle(userdata, true); + + if (objectHandle.GetTarget(out var context)) + { + context.Dispose(); + } + } + + private class ClipboardCallbackContext : IDisposable + { + public readonly string MimeType; + + /// + /// Provider of data suitable for the . + /// + /// Called when another application requests that mime type from the OS clipboard. + private Func>? dataProvider; + + private MemoryHandle memoryHandle; + + /// + /// Address of the returned by the . + /// + /// Pinned and suitable for passing to unmanaged code. + public unsafe IntPtr Address => (IntPtr)memoryHandle.Pointer; + + /// + /// Length of the returned by the . + /// + public UIntPtr DataLength { get; private set; } + + public ClipboardCallbackContext(string mimeType, Func> 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(); + } + + public void Dispose() + { + memoryHandle.Dispose(); + DataLength = 0; + } } } } diff --git a/osu.Framework/Platform/SDL3GameHost.cs b/osu.Framework/Platform/SDL3GameHost.cs index 345c06a13b..22267963d6 100644 --- a/osu.Framework/Platform/SDL3GameHost.cs +++ b/osu.Framework/Platform/SDL3GameHost.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Touch; using osu.Framework.Platform.SDL; +using SixLabors.ImageSharp.Formats.Png; namespace osu.Framework.Platform { @@ -31,7 +32,8 @@ protected override TextInputSource CreateTextInput() return base.CreateTextInput(); } - protected override Clipboard CreateClipboard() => new SDL3Clipboard(); + // PNG works well on linux + protected override Clipboard CreateClipboard() => new SDL3Clipboard(PngFormat.Instance); protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[]