Skip to content

Instantly share code, notes, and snippets.

@darkerbit
Created January 28, 2025 15:23
Show Gist options
  • Select an option

  • Save darkerbit/3ff51943fa169635f9f5ce2f25e01a71 to your computer and use it in GitHub Desktop.

Select an option

Save darkerbit/3ff51943fa169635f9f5ce2f25e01a71 to your computer and use it in GitHub Desktop.
C# wrapper for the SDL file dialog API... 2!
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