pub const c = @cImport({ @cInclude("nfd.h"); }); const std = @import("std"); const p2 = @import("p2"); pub const NFDCallbackError = error{ InvalidPath, Unknown, }; const NFDCallbackFn = *const fn ( ?*anyopaque, ?[]const u8, ) void; pub const NFDRuntimeOptions = struct { singleThreaded: bool = false, }; pub const AsyncOpenFileDialogArgs = struct { filterList: []const u8 = "", defaultPath: []const u8 = "", callback: NFDCallbackFn, callbackContext: ?*anyopaque = null, }; // if we need thread-safe callback events on a specific thread thats not the main thread, then we will need the callbacks itself to be // handled on the caller's thread pub const AsyncNfdCallbackEvent = struct { path: ?[]const u8, context: ?*anyopaque, func: NFDCallbackFn, }; // There are a couple of incredibly annoying things about NFD. // this is only for running things in multiple threads. pub const NFDRuntime = struct { allocator: std.mem.Allocator, lock: std.Thread.Mutex = .{}, outPath: [*c]u8 = null, nfdPathSet: c.nfdpathset_t = undefined, callbackEvents: p2.ConcurrentQueueU(AsyncNfdCallbackEvent), opts: NFDRuntimeOptions = .{}, pathsArena: std.heap.ArenaAllocator, runtimeState: union(enum(u8)) { none: bool, openDialog: struct { filterList: []const u8, defaultPath: []const u8, callback: ?NFDCallbackFn = null, callbackContext: ?*anyopaque = null, }, openDialogMultiple: struct { filterList: []const u8, defaultPath: []const u8, }, saveDialog: struct { filterList: []const u8, defaultPath: []const u8, }, openFolder: struct { defaultPath: []const u8, callback: ?NFDCallbackFn = null, callbackContext: ?*anyopaque = null, }, } = .{ .none = false }, pub fn create(allocator: std.mem.Allocator, options: NFDRuntimeOptions) !*@This() { const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, .callbackEvents = try p2.ConcurrentQueueU(AsyncNfdCallbackEvent).initCapacity(allocator, 256), .opts = options, .pathsArena = std.heap.ArenaAllocator.init(allocator), }; return self; } pub fn openFolderDialog(self: *@This(), defaultPath: []const u8) ![*c]const u8 { self.lock.lock(); std.debug.assert(self.runtimeState == .none); self.runtimeState = .{ .openFolder = .{ .defaultPath = defaultPath, } }; self.lock.unlock(); self.blockUntilComplete(); return self.outPath; } pub fn openFileDialog(self: *@This(), defaultPath: []const u8, filters: []const u8) ![*c]const u8 { self.lock.lock(); std.debug.assert(self.runtimeState == .none); self.runtimeState = .{ .openDialog = .{ .filterList = filters, .defaultPath = defaultPath, } }; self.lock.unlock(); // a few sleeps here massively decreases cpu block time. self.blockUntilComplete(); return self.outPath; } pub fn asyncOpenFolderDialog(self: *@This(), args: AsyncOpenFileDialogArgs) !void { self.lock.lock(); defer self.lock.unlock(); switch (self.runtimeState) { .none => { self.runtimeState = .{ .openFolder = .{ .defaultPath = args.defaultPath, .callback = args.callback, .callbackContext = args.callbackContext, } }; }, .openDialog, .openDialogMultiple, .saveDialog, .openFolder => { // return dialog already opened error. nfd would returl NFD_ERROR here return error.DialogAlreadyOpen; }, } } pub fn asyncOpenFileDialog(self: *@This(), args: AsyncOpenFileDialogArgs) !void { self.lock.lock(); defer self.lock.unlock(); switch (self.runtimeState) { .none => { self.runtimeState = .{ .openDialog = .{ .filterList = args.filterList, .defaultPath = args.defaultPath, .callback = args.callback, .callbackContext = args.callbackContext, } }; }, .openDialog, .openDialogMultiple, .saveDialog, .openFolder => { // return dialog already opened error. nfd would returl NFD_ERROR here return error.DialogAlreadyOpen; }, } } fn blockUntilComplete(self: *@This()) void { std.time.sleep(1000 * 1000); var spinning: bool = true; while (spinning) { self.lock.lock(); if (self.runtimeState == .none) { spinning = false; } std.time.sleep(1000 * 1000); self.lock.unlock(); } } pub fn processMessages(self: *@This()) !void { self.lock.lock(); defer self.lock.unlock(); switch (self.runtimeState) { .none => {}, .openDialog => |args| { const res = c.NFD_OpenDialog(args.filterList.ptr, args.defaultPath.ptr, &self.outPath); if (res == c.NFD_CANCEL) { self.outPath = null; } else if (res == c.NFD_ERROR) { return error.InvalidFileDialog; } if (args.callback) |callback| { if (self.outPath != null) { try self.callbackEvents.push(.{ .path = try p2.dupeString(self.pathsArena.allocator(), std.mem.span(self.outPath)), .context = args.callbackContext, .func = callback, }); } else { try self.callbackEvents.push(.{ .path = null, .context = args.callbackContext, .func = callback, }); } } self.runtimeState = .{ .none = false }; }, .openDialogMultiple => { return error.NotImplemented; }, .saveDialog => { return error.NotImplemented; }, .openFolder => |args| { const res = c.NFD_PickFolder(args.defaultPath.ptr, &self.outPath); if (res == c.NFD_CANCEL) { self.outPath = null; } else if (res == c.NFD_ERROR) { return error.InvalidFileDialog; } if (args.callback) |callback| { if (self.outPath != null) { try self.callbackEvents.push(.{ .path = try p2.dupeString(self.pathsArena.allocator(), std.mem.span(self.outPath)), .context = args.callbackContext, .func = callback, }); } else { try self.callbackEvents.push(.{ .path = null, .context = args.callbackContext, .func = callback, }); } } self.runtimeState = .{ .none = false }; }, } } pub fn processCallbacks(self: *@This()) void { while (self.callbackEvents.pop()) |event| { event.func(event.context, event.path); } } pub fn destroy(self: *@This()) void { self.callbackEvents.deinit(self.allocator); self.pathsArena.deinit(); self.allocator.destroy(self); } }; test "nfd simple test" { var runtime = try NFDRuntime.create(std.testing.allocator, .{ .singleThreaded = true }); defer runtime.destroy(); }