From 028a90e5ea5295753398b343b1032293630ee119 Mon Sep 17 00:00:00 2001 From: Grant Karapetyan Date: Sun, 11 Jan 2026 14:47:37 +0300 Subject: [PATCH 1/5] Support drag&drop directories for GLFW Change in GLFW.onDrop, when filesystem is enabled to support dropping directories --- src/lib/libglfw.js | 76 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/lib/libglfw.js b/src/lib/libglfw.js index bd0e4c8dc4db8..e90e075c2ffef 100644 --- a/src/lib/libglfw.js +++ b/src/lib/libglfw.js @@ -838,28 +838,33 @@ var LibraryGLFW = { event.preventDefault(); #if FILESYSTEM + var drop_dir = '.glfw_dropped_files'; var filenames = _malloc(event.dataTransfer.files.length * {{{ POINTER_SIZE }}}); var filenamesArray = []; - var count = event.dataTransfer.files.length; + for (var i = 0; i < event.dataTransfer.files.length; ++i) { + var path = "/" + drop_dir + "/" + event.dataTransfer.files[i].name.replace(/\//g, "_"); + var filename = stringToNewUTF8(path); + filenamesArray.push(filename); + {{{ makeSetValue('filenames', `i*${POINTER_SIZE}` , 'filename', '*') }}}; + } // Read and save the files to emscripten's FS var written = 0; - var drop_dir = '.glfw_dropped_files'; FS.createPath('/', drop_dir); - function save(file) { - var path = '/' + drop_dir + '/' + file.name.replace(/\//g, '_'); + function save(file, in_path, numfiles) { + var path = '/' + drop_dir + in_path + '/' + file.name.replace(/\//g, '_'); var reader = new FileReader(); reader.onloadend = (e) => { if (reader.readyState != 2) { // not DONE ++written; - out('failed to read dropped file: '+file.name+': '+reader.error); + out('failed to read dropped file: ' +in_path+'/'+file.name+': '+reader.error); return; } var data = e.target.result; FS.writeFile(path, new Uint8Array(data)); - if (++written === count) { + if (++written === numfiles) { {{{ makeDynCall('vpip', 'GLFW.active.dropFunc') }}}(GLFW.active.id, count, filenames); for (var i = 0; i < filenamesArray.length; ++i) { @@ -869,14 +874,61 @@ var LibraryGLFW = { } }; reader.readAsArrayBuffer(file); - - var filename = stringToNewUTF8(path); - filenamesArray.push(filename); - {{{ makeSetValue('filenames', `i*${POINTER_SIZE}` , 'filename', '*') }}}; } - for (var i = 0; i < count; ++i) { - save(event.dataTransfer.files[i]); + let filesQ = []; + function finalize() { + var count = filesQ.length; + for (var i = 0; i < count; ++i) + save(filesQ[i].file, filesQ[i].path, count); + } + + if (typeof DataTransferItem.prototype.webkitGetAsEntry !== "undefined") { + let entriesTree = {}; + var markDone = function (fullpath, recursive) { + if (entriesTree[fullpath].subpaths.length != 0) return; + delete entriesTree[fullpath]; + let parentpath = fullpath.substring(0, fullpath.lastIndexOf('/')); + if (!entriesTree.hasOwnProperty(parentpath)) { + if (Object.keys(entriesTree).length == 0) finalize(); + return; + } + const fpIndex = entriesTree[parentpath].subpaths.indexOf(fullpath); + if (fpIndex > -1) entriesTree[parentpath].subpaths.splice(fpIndex, 1); + if (recursive) markDone(parentpath, true); + if (Object.keys(entriesTree).length == 0) finalize(); + }; + var processEntry = function (entry) { + let fp = entry.fullPath; + let pp = fp.substring(0, fp.lastIndexOf('/')); + entriesTree[fp] = { subpaths: [] }; + if (entry.isFile) { + entry.file((f) => { filesQ.push({ file: f, path: pp }); markDone(fp, false); }) + } else if (entry.isDirectory) { + if (entriesTree.hasOwnProperty(pp)) entriesTree[pp].subpaths.push(fp); + FS.createPath("/" + drop_dir + pp, entry.name); + var reader = entry.createReader(); + var rRead = function (dirEntries) { + if (dirEntries.length == 0) { + markDone(fp, true); + return; + } + for (const ent of dirEntries) processEntry(ent); + reader.readEntries(rRead); + }; + reader.readEntries(rRead); + } + }; + for (const item of event.dataTransfer.items) { + processEntry(item.webkitGetAsEntry()); + } + } + else { + // fallback for browsers that does not support `webkitGetAsEntry` + for (var i = 0; i < event.dataTransfer.files.length; ++i) { + filesQ.push({ file: event.dataTransfer.files[i], path: "" }); + } + finalize(); } #endif // FILESYSTEM From b900815c7766df10790a179cb63ca1cc3d862fb9 Mon Sep 17 00:00:00 2001 From: Grant Karapetyan Date: Sun, 11 Jan 2026 18:27:34 +0300 Subject: [PATCH 2/5] CR fixes --- src/lib/libglfw.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/libglfw.js b/src/lib/libglfw.js index e90e075c2ffef..768059dc65e8b 100644 --- a/src/lib/libglfw.js +++ b/src/lib/libglfw.js @@ -842,7 +842,7 @@ var LibraryGLFW = { var filenames = _malloc(event.dataTransfer.files.length * {{{ POINTER_SIZE }}}); var filenamesArray = []; for (var i = 0; i < event.dataTransfer.files.length; ++i) { - var path = "/" + drop_dir + "/" + event.dataTransfer.files[i].name.replace(/\//g, "_"); + var path = `/${drop_dir}/${event.dataTransfer.files[i].name.replace(/\//g, "_")}`; var filename = stringToNewUTF8(path); filenamesArray.push(filename); {{{ makeSetValue('filenames', `i*${POINTER_SIZE}` , 'filename', '*') }}}; @@ -858,7 +858,7 @@ var LibraryGLFW = { reader.onloadend = (e) => { if (reader.readyState != 2) { // not DONE ++written; - out('failed to read dropped file: ' +in_path+'/'+file.name+': '+reader.error); + err(`failed to read dropped file: ${in_path}/${file.name}: ${reader.error}`); return; } @@ -879,13 +879,14 @@ var LibraryGLFW = { let filesQ = []; function finalize() { var count = filesQ.length; - for (var i = 0; i < count; ++i) + for (var i = 0; i < count; ++i) { save(filesQ[i].file, filesQ[i].path, count); + } } if (typeof DataTransferItem.prototype.webkitGetAsEntry !== "undefined") { let entriesTree = {}; - var markDone = function (fullpath, recursive) { + function markDone(fullpath, recursive) { if (entriesTree[fullpath].subpaths.length != 0) return; delete entriesTree[fullpath]; let parentpath = fullpath.substring(0, fullpath.lastIndexOf('/')); @@ -898,7 +899,7 @@ var LibraryGLFW = { if (recursive) markDone(parentpath, true); if (Object.keys(entriesTree).length == 0) finalize(); }; - var processEntry = function (entry) { + function processEntry(entry) { let fp = entry.fullPath; let pp = fp.substring(0, fp.lastIndexOf('/')); entriesTree[fp] = { subpaths: [] }; @@ -922,11 +923,10 @@ var LibraryGLFW = { for (const item of event.dataTransfer.items) { processEntry(item.webkitGetAsEntry()); } - } - else { - // fallback for browsers that does not support `webkitGetAsEntry` - for (var i = 0; i < event.dataTransfer.files.length; ++i) { - filesQ.push({ file: event.dataTransfer.files[i], path: "" }); + } else { + // fallback for browsers that does not support webkitGetAsEntry + for (const file of event.dataTransfer.files) { + filesQ.push({ file: file, path: "" }); } finalize(); } From 016445a6a293baf3789d7a8480f853ca4af1f3d7 Mon Sep 17 00:00:00 2001 From: Grant Karapetyan Date: Sun, 11 Jan 2026 20:11:41 +0300 Subject: [PATCH 3/5] CR/CI Fix Use shorter version for `DataTransferItem.prototype.webkitAsEntry` check Fix typo using `count` instead of `filenamesArray.length` --- src/lib/libglfw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/libglfw.js b/src/lib/libglfw.js index 768059dc65e8b..0c6d67a664f76 100644 --- a/src/lib/libglfw.js +++ b/src/lib/libglfw.js @@ -865,7 +865,7 @@ var LibraryGLFW = { var data = e.target.result; FS.writeFile(path, new Uint8Array(data)); if (++written === numfiles) { - {{{ makeDynCall('vpip', 'GLFW.active.dropFunc') }}}(GLFW.active.id, count, filenames); + {{{ makeDynCall('vpip', 'GLFW.active.dropFunc') }}}(GLFW.active.id, filenamesArray.length, filenames); for (var i = 0; i < filenamesArray.length; ++i) { _free(filenamesArray[i]); @@ -884,7 +884,7 @@ var LibraryGLFW = { } } - if (typeof DataTransferItem.prototype.webkitGetAsEntry !== "undefined") { + if (DataTransferItem.prototype.webkitGetAsEntry) { let entriesTree = {}; function markDone(fullpath, recursive) { if (entriesTree[fullpath].subpaths.length != 0) return; From b3e2cf8ff8fd532fb88f234a32901cf6bffed55e Mon Sep 17 00:00:00 2001 From: Grant Karapetyan Date: Mon, 12 Jan 2026 12:13:43 +0300 Subject: [PATCH 4/5] CR Fix + test update --- src/lib/libglfw.js | 4 +-- test/interactive/test_glfw_dropfile.c | 36 +++++++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/lib/libglfw.js b/src/lib/libglfw.js index 0c6d67a664f76..efc5e96bf7371 100644 --- a/src/lib/libglfw.js +++ b/src/lib/libglfw.js @@ -898,7 +898,7 @@ var LibraryGLFW = { if (fpIndex > -1) entriesTree[parentpath].subpaths.splice(fpIndex, 1); if (recursive) markDone(parentpath, true); if (Object.keys(entriesTree).length == 0) finalize(); - }; + } function processEntry(entry) { let fp = entry.fullPath; let pp = fp.substring(0, fp.lastIndexOf('/')); @@ -919,7 +919,7 @@ var LibraryGLFW = { }; reader.readEntries(rRead); } - }; + } for (const item of event.dataTransfer.items) { processEntry(item.webkitGetAsEntry()); } diff --git a/test/interactive/test_glfw_dropfile.c b/test/interactive/test_glfw_dropfile.c index 46da920bfb831..11f8b949f4a04 100644 --- a/test/interactive/test_glfw_dropfile.c +++ b/test/interactive/test_glfw_dropfile.c @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -25,29 +26,44 @@ void render() { glClear(GL_COLOR_BUFFER_BIT); } -void on_file_drop(GLFWwindow *window, int count, const char **paths) { - for (int i = 0; i < count; ++i) { - printf("dropped file %s\n", paths[i]); - - FILE *fp = fopen(paths[i], "rb"); +int display_info(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftwbuf) { + printf("%-3s %2d %s\n", + (tflag == FTW_D) ? "d" : "f", // Type: directory or file + ftwbuf->level, // Depth level + fpath // Full path + ); + if ( tflag != FTW_D ) { + FILE *fp = fopen(fpath, "rb"); if (!fp) { - printf("failed to open %s\n", paths[i]); + printf("failed to open %s\n", fpath); perror("fopen"); - assert(false); + return -1; } int c; long size = 0; - bool dump = strstr(paths[i], ".txt") != 0; + bool dump = strstr(fpath, ".txt") != 0; if (dump) printf("text file contents (first 100 bytes): "); while ((c = fgetc(fp)) != -1) { ++size; if (dump && size <= 100) putchar(c); } if (dump) putchar('\n'); - printf("read %ld bytes from %s\n", size, paths[i]); + printf("read %ld bytes from %s\n", size, fpath); fclose(fp); + } + return 0; // Return 0 to continue traversal +} +void on_file_drop(GLFWwindow *window, int count, const char **paths) { + for (int i = 0; i < count; ++i) { + printf("dropped file %s\n", paths[i]); + int flags = FTW_PHYS; // Do not follow symbolic links + if (nftw(paths[i], display_info, 20, flags) == -1) { + printf("failed to traverse %s\n", paths[i]); + perror("nftw"); + assert(false); + } #ifdef __EMSCRIPTEN__ // Emscripten copies the contents of the dropped file into the // in-browser filesystem. Delete after usage to free up memory. @@ -79,7 +95,7 @@ int main() { glfwSetDropCallback(g_window, on_file_drop); // Main loop - printf("Drag and drop a file from your desktop onto the green canvas.\n"); + printf("Drag and drop a file or directory from your desktop onto the green canvas.\n"); #ifdef __EMSCRIPTEN__ emscripten_set_main_loop(render, 0, 1); #else From 09dd56c255a432453c6d89a17ca1c779cd72e005 Mon Sep 17 00:00:00 2001 From: Grant Karapetyan Date: Mon, 12 Jan 2026 19:11:00 +0300 Subject: [PATCH 5/5] CR Fix --- test/interactive/test_glfw_dropfile.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/interactive/test_glfw_dropfile.c b/test/interactive/test_glfw_dropfile.c index 11f8b949f4a04..aaad330bd8d69 100644 --- a/test/interactive/test_glfw_dropfile.c +++ b/test/interactive/test_glfw_dropfile.c @@ -58,8 +58,8 @@ int display_info(const char *fpath, const struct stat *sb, int tflag, struct FTW void on_file_drop(GLFWwindow *window, int count, const char **paths) { for (int i = 0; i < count; ++i) { printf("dropped file %s\n", paths[i]); - int flags = FTW_PHYS; // Do not follow symbolic links - if (nftw(paths[i], display_info, 20, flags) == -1) { + // FTW_PHYS: Do not follow symbolic links + if (nftw(paths[i], display_info, 20, FTW_PHYS) == -1) { printf("failed to traverse %s\n", paths[i]); perror("nftw"); assert(false);