Created
January 28, 2025 15:23
-
-
Save darkerbit/3ff51943fa169635f9f5ce2f25e01a71 to your computer and use it in GitHub Desktop.
C# wrapper for the SDL file dialog API... 2!
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.Collections.Concurrent; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using MoonWorks; | |
| using SDL3; | |
| namespace Segadora.UI; | |
| public unsafe class FileDialog : IDisposable | |
| { | |
| // Prevent active file dialogs from being GC'd | |
| private static ConcurrentDictionary<IntPtr, FileDialog> activeDialogs = | |
| new ConcurrentDictionary<IntPtr, FileDialog>(); | |
| public delegate void Callback(string[] filePaths, int filter); | |
| public readonly record struct Filter(string Name, string Pattern); | |
| public static void OpenFile( | |
| Callback callback, | |
| Filter[] filters, | |
| bool allowMany = false | |
| ) | |
| { | |
| FileDialog dialog = new FileDialog(callback); | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = dialog.AllocateFilters(filters); | |
| activeDialogs[dialog.UserdataKey] = dialog; | |
| SDL.SDL_ShowOpenFileDialog( | |
| SDLCallback, | |
| dialog.UserdataKey, | |
| IntPtr.Zero, | |
| filterSpan, | |
| filterSpan.Length, | |
| null, // todo: get default_location working on Windows | |
| allowMany | |
| ); | |
| } | |
| public static void SaveFile( | |
| Callback callback, | |
| Filter[] filters | |
| ) | |
| { | |
| FileDialog dialog = new FileDialog(callback); | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = dialog.AllocateFilters(filters); | |
| activeDialogs[dialog.UserdataKey] = dialog; | |
| SDL.SDL_ShowSaveFileDialog( | |
| SDLCallback, | |
| dialog.UserdataKey, | |
| IntPtr.Zero, | |
| filterSpan, | |
| filterSpan.Length, | |
| null // todo: get default_location working on Windows | |
| ); | |
| } | |
| public static void OpenFolder( | |
| Callback callback, | |
| bool allowMany = false | |
| ) | |
| { | |
| FileDialog dialog = new FileDialog(callback); | |
| // We need a filter memory block allocation for the userdata key... | |
| // ...so just give it a bogus filter to allocate | |
| dialog.AllocateFilters([ | |
| new Filter("Bogus", "b"), | |
| ]); | |
| activeDialogs[dialog.UserdataKey] = dialog; | |
| SDL.SDL_ShowOpenFolderDialog( | |
| SDLCallback, | |
| dialog.UserdataKey, | |
| IntPtr.Zero, | |
| null, // todo: get default_location working on Windows | |
| allowMany | |
| ); | |
| } | |
| private static void SDLCallback(IntPtr userdata, IntPtr filelist, int filter) | |
| { | |
| activeDialogs.TryRemove(userdata, out FileDialog dialog); | |
| if (userdata == IntPtr.Zero) | |
| { | |
| Logger.LogError($"File dialog callback failed ({SDL.SDL_GetError()})"); | |
| dialog?.UserCallback(null, 0); | |
| dialog?.Dispose(); | |
| return; | |
| } | |
| IntPtr filePathPtr = filelist; | |
| int filePathCount = 0; | |
| while (Marshal.ReadIntPtr(filePathPtr) != IntPtr.Zero) | |
| { | |
| filePathCount++; | |
| filePathPtr += IntPtr.Size; | |
| } | |
| if (filePathCount == 0) | |
| { | |
| dialog?.UserCallback(null, filter); | |
| dialog?.Dispose(); | |
| return; | |
| } | |
| string[] filePaths = new string[filePathCount]; | |
| filePathPtr = filelist; | |
| for (int i = 0; i < filePathCount; i++) | |
| { | |
| filePaths[i] = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(filePathPtr)); | |
| filePathPtr += IntPtr.Size; | |
| } | |
| dialog?.UserCallback(filePaths, filter); | |
| dialog?.Dispose(); | |
| } | |
| public Callback UserCallback { get; } | |
| public IntPtr UserdataKey => new IntPtr(block); | |
| /* | |
| * Big pinned memory block for the filter list and strings | |
| * | |
| * SDL requires the unmanaged filter list pointer and the unmanaged strings to be | |
| * valid until the callback fires, so I'm just allocating a big block of memory | |
| * for keeping them around in | |
| */ | |
| private byte* block; | |
| private FileDialog(Callback callback) | |
| { | |
| UserCallback = callback; | |
| } | |
| public Span<SDL.SDL_DialogFileFilter> AllocateFilters(Filter[] filters) | |
| { | |
| long blockLength = filters.Length * sizeof(SDL.SDL_DialogFileFilter); | |
| foreach (Filter filter in filters) | |
| { | |
| blockLength += Encoding.UTF8.GetByteCount(filter.Name) + 1; | |
| blockLength += Encoding.UTF8.GetByteCount(filter.Pattern) + 1; | |
| } | |
| block = (byte*) NativeMemory.Alloc((nuint) blockLength); | |
| // Put the filter list at the beginning of the block... | |
| Span<SDL.SDL_DialogFileFilter> filterSpan = new Span<SDL.SDL_DialogFileFilter>(block, filters.Length); | |
| // ...and the strings after | |
| long stringOffset = filters.Length * sizeof(SDL.SDL_DialogFileFilter); | |
| for (int i = 0; i < filters.Length; i++) | |
| { | |
| byte* name = block + stringOffset; | |
| long nameLength = Encoding.UTF8.GetByteCount(filters[i].Name) + 1; | |
| Encoding.UTF8.GetBytes(filters[i].Name, new Span<byte>(name, (int) nameLength - 1)); | |
| new Span<byte>(name, (int) nameLength)[(int) nameLength - 1] = 0; | |
| stringOffset += nameLength; | |
| byte* pattern = block + stringOffset; | |
| long patternLength = Encoding.UTF8.GetByteCount(filters[i].Pattern) + 1; | |
| Encoding.UTF8.GetBytes(filters[i].Pattern, new Span<byte>(pattern, (int) patternLength - 1)); | |
| new Span<byte>(pattern, (int) patternLength)[(int) patternLength - 1] = 0; | |
| stringOffset += patternLength; | |
| filterSpan[i] = new SDL.SDL_DialogFileFilter | |
| { | |
| name = name, | |
| pattern = pattern, | |
| }; | |
| } | |
| return filterSpan; | |
| } | |
| protected virtual void Dispose(bool disposing) | |
| { | |
| if (disposing) | |
| { | |
| } | |
| if (block != null) | |
| { | |
| NativeMemory.Free(block); | |
| } | |
| } | |
| ~FileDialog() | |
| { | |
| Dispose(false); | |
| } | |
| public void Dispose() | |
| { | |
| Dispose(true); | |
| GC.SuppressFinalize(this); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment