Created
January 26, 2025 09:51
-
-
Save darkerbit/a4a557c3ff0db5556b7bd661c2082c9f to your computer and use it in GitHub Desktop.
C# wrapper for the SDL3 file dialog API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using SDL3; | |
| namespace Segadora.UI; | |
| public static class SDLFileDialogs | |
| { | |
| private unsafe class DialogData : IDisposable | |
| { | |
| public DialogFileCallback Callback { get; } | |
| /* | |
| * Memory block containing the filters and the strings used by the filters | |
| * | |
| * SDL requires validity of the filter pointer and the strings until the callback is triggered | |
| * which is a very annoying lifetime requirement to deal with in C#, so I'm just wrangling | |
| * a big old block of unmanaged memory by hand for it. | |
| * | |
| * There's probably a better way to do this. Too bad! | |
| */ | |
| private byte* buffer; | |
| public DialogData(DialogFileCallback callback) | |
| { | |
| Callback = callback; | |
| } | |
| public Span<SDL.SDL_DialogFileFilter> AllocFilters(DialogFileFilter[] filters) | |
| { | |
| if (filters == null) | |
| { | |
| return Span<SDL.SDL_DialogFileFilter>.Empty; | |
| } | |
| long bufferSize = filters.Length * sizeof(SDL.SDL_DialogFileFilter); | |
| foreach (DialogFileFilter filter in filters) | |
| { | |
| bufferSize += Encoding.UTF8.GetByteCount(filter.name) + 1; | |
| bufferSize += Encoding.UTF8.GetByteCount(filter.pattern) + 1; | |
| } | |
| buffer = (byte*) NativeMemory.Alloc((nuint) bufferSize); | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = new Span<SDL.SDL_DialogFileFilter>(buffer, filters.Length); | |
| long bufferOffset = filters.Length * sizeof(SDL.SDL_DialogFileFilter); | |
| for (int i = 0; i < filters.Length; i++) | |
| { | |
| byte* name = buffer + bufferOffset; | |
| long nameLength = Encoding.UTF8.GetByteCount(filters[i].name) + 1; | |
| bufferOffset += nameLength; | |
| byte* pattern = buffer + bufferOffset; | |
| long patternLength = Encoding.UTF8.GetByteCount(filters[i].pattern) + 1; | |
| bufferOffset += patternLength; | |
| Encoding.UTF8.GetBytes(filters[i].name, new Span<byte>(name, (int) nameLength - 1)); | |
| new Span<byte>(name, (int) nameLength)[(int) nameLength - 1] = 0; | |
| Encoding.UTF8.GetBytes(filters[i].pattern, new Span<byte>(pattern, (int) patternLength - 1)); | |
| new Span<byte>(pattern, (int) patternLength)[(int) patternLength - 1] = 0; | |
| filterSpan[i] = new SDL.SDL_DialogFileFilter | |
| { | |
| name = name, | |
| pattern = pattern, | |
| }; | |
| } | |
| return filterSpan; | |
| } | |
| protected virtual void Dispose(bool disposing) | |
| { | |
| if (disposing) | |
| { | |
| } | |
| if (buffer != null) | |
| { | |
| NativeMemory.Free(buffer); | |
| } | |
| } | |
| ~DialogData() | |
| { | |
| Dispose(false); | |
| } | |
| public void Dispose() | |
| { | |
| Dispose(true); | |
| GC.SuppressFinalize(this); | |
| } | |
| } | |
| public delegate void DialogFileCallback(string[] files, int filter); | |
| public record struct DialogFileFilter(string name, string pattern); | |
| public static void ShowOpenFileDialog( | |
| DialogFileCallback callback, | |
| DialogFileFilter[] filters = null, | |
| bool allowMany = false | |
| ) | |
| { | |
| DialogData data = new DialogData(callback); | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = data.AllocFilters(filters); | |
| GCHandle gch = GCHandle.Alloc(data); | |
| SDL.SDL_ShowOpenFileDialog( | |
| SDLFileCallback, | |
| GCHandle.ToIntPtr(gch), | |
| IntPtr.Zero, | |
| filterSpan, | |
| filterSpan.Length, | |
| null, // doesn't work on Windows, todo: figure out why | |
| allowMany | |
| ); | |
| } | |
| public static void ShowSaveFileDialog( | |
| DialogFileCallback callback, | |
| DialogFileFilter[] filters = null | |
| ) | |
| { | |
| DialogData data = new DialogData(callback); | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = data.AllocFilters(filters); | |
| GCHandle gch = GCHandle.Alloc(data); | |
| SDL.SDL_ShowSaveFileDialog( | |
| SDLFileCallback, | |
| GCHandle.ToIntPtr(gch), | |
| IntPtr.Zero, | |
| filterSpan, | |
| filterSpan.Length, | |
| null | |
| ); | |
| } | |
| public static void ShowOpenFolderDialog( | |
| DialogFileCallback callback, | |
| bool allowMany = false | |
| ) | |
| { | |
| DialogData data = new DialogData(callback); | |
| GCHandle gch = GCHandle.Alloc(data); | |
| SDL.SDL_ShowOpenFolderDialog( | |
| SDLFileCallback, | |
| GCHandle.ToIntPtr(gch), | |
| IntPtr.Zero, | |
| null, | |
| allowMany | |
| ); | |
| } | |
| private static void SDLFileCallback(IntPtr userdata, IntPtr filelist, int filter) | |
| { | |
| GCHandle gch = GCHandle.FromIntPtr(userdata); | |
| DialogData data = (DialogData) gch.Target; | |
| gch.Free(); | |
| if (filelist == IntPtr.Zero) | |
| { | |
| throw new Exception(SDL.SDL_GetError()); | |
| } | |
| // What follows is some stupidity. | |
| // todo: Rewrite everything | |
| int fileCount = 0; | |
| IntPtr filePathPtr = filelist; | |
| while (Marshal.ReadIntPtr(filePathPtr) != IntPtr.Zero) | |
| { | |
| fileCount++; | |
| filePathPtr += Marshal.SizeOf<IntPtr>(); | |
| } | |
| if (fileCount == 0) | |
| { | |
| data?.Callback(null, filter); | |
| data?.Dispose(); | |
| return; | |
| } | |
| string[] filePaths = new string[fileCount]; | |
| filePathPtr = filelist; | |
| for (int i = 0; i < fileCount; i++) | |
| { | |
| filePaths[i] = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(filePathPtr)); | |
| filePathPtr += Marshal.SizeOf<IntPtr>(); | |
| } | |
| data?.Callback(filePaths, filter); | |
| data?.Dispose(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment