Skip to content

Instantly share code, notes, and snippets.

@darkerbit
Created January 26, 2025 09:51
Show Gist options
  • Select an option

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

Select an option

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