Skip to content

Instantly share code, notes, and snippets.

@adamkewley
Created January 28, 2025 11:45
Show Gist options
  • Save adamkewley/d0847228e93dcdb2a1d9a5c679f170a5 to your computer and use it in GitHub Desktop.
Save adamkewley/d0847228e93dcdb2a1d9a5c679f170a5 to your computer and use it in GitHub Desktop.
void prompt_user_to_select_file_async(
std::function<void(FileDialogResponse)> callback,
std::span<const FileDialogFilter> filters,
std::optional<std::filesystem::path> initial_directory_to_show,
bool allow_many)
{
// State that's stored in the sdl3 callback.
struct SDL3CallbackState final {
// Constructs the callback state that's stored in SDL3's dialog system.
explicit SDL3CallbackState(
std::function<void(FileDialogResponse)>&& callback_,
std::span<const FileDialogFilter> filters_) :
caller_callback{std::move(callback_)},
caller_filters(filters_.begin(), filters_.end())
{
// The caller's filters are lifetime-controlled (`std::string`s), the SDL
// filters are not-lifetime-controlled views (`const char*`s). The SDL3
// API for `SDL_ShowOpenFileDialog` mandates that "the filters' data
// must be valid at least until `sdl3_callback` is called", so we keep
// both alive.
sdl3_filters.reserve(caller_filters.size());
for (const FileDialogFilter& filter : caller_filters) {
sdl3_filters.push_back(SDL_DialogFileFilter{
.name = filter.name().c_str(),
.pattern = filter.pattern().c_str(),
});
}
}
std::function<void(FileDialogResponse)> caller_callback;
std::vector<FileDialogFilter> caller_filters;
std::vector<SDL_DialogFileFilter> sdl3_filters;
};
// This free function is what SDL calls with `SDL3CallbackState` when the user is
// finished with the dialog.
const auto sdl3_callback = [](void* userdata, const char* const* filelist, int) -> void
{
// Unpack callback state.
const std::unique_ptr<SDL3CallbackState> state{static_cast<SDL3CallbackState*>(userdata)};
// If there's an error, emit a `FileDialogResponse` that contains the error.
if (not filelist) {
App::upd().request_invoke_on_main_thread([caller_callback = std::move(state->caller_callback), response = FileDialogResponse{SDL_GetError()}]()
{
caller_callback(response);
});
return;
}
// Convert SDL's file list to an oscar `FileDialogResponse`
std::vector<std::filesystem::path> files;
while (*filelist) {
files.emplace_back(*filelist);
++filelist;
}
// Marshal the call to the user's callback onto the main thread by packing it
// into an `AppMarshalledCallbackEvent`.
App::upd().request_invoke_on_main_thread([caller_callback = std::move(state->caller_callback), response = FileDialogResponse{std::move(files)}]()
{
// Call the user's callback (the event's callback happens on the main thread).
caller_callback(response);
});
};
// Setup `SDL_ShowOpenFileDialog` arguments.
auto sdl3_callback_state = std::make_unique<SDL3CallbackState>(std::move(callback), filters);
const SDL_DialogFileFilter* sdl3_filters_ptr = sdl3_callback_state->sdl3_filters.data();
const auto sdl3_num_filters = static_cast<int>(sdl3_callback_state->sdl3_filters.size());
std::string default_location;
if (initial_directory_to_show) {
default_location = initial_directory_to_show->string();
}
else if (const auto fallback = get_initial_directory_to_show_fallback()) {
default_location = fallback->string();
}
// Call into SDL3's dialog implementation.
SDL_ShowOpenFileDialog(
sdl3_callback,
sdl3_callback_state.release(),
main_window_.get(), // make it modal in the main window
sdl3_filters_ptr,
sdl3_num_filters,
default_location.empty() ? nullptr : default_location.c_str(),
allow_many
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment