Closed cschar closed 1 month ago
(deleted my previous comment because I misunderstood your question, I thought you wanted to download a file from a server, not from the webpage to the local file system).
Long story short, the sokol headers have no support for saving a file to the local filesystem, and the Emscripten C stdlib won't help you either AFAIK.
You'll need to write your own Javascript code which either creates and 'clicks' a download link (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download), I do this here in the visual6502remix project: https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/util.c#L40-L54) - since this emulates a click you'll need to call this function from inside a 'short-lived HTML event handler' though.
The other option is the new-ish HTML filesystem API:
https://developer.mozilla.org/en-US/docs/Web/API/File_System_API
I haven't used this yet though.
But in any case, welcome to web development, where the simplest things are extremely difficult ;)
Ok found a working solution for anyone in future:
had to modify CMakeLists.txt just a bit to be less restrictive
demo.c
//------------------------------------------------------------------------------
// Simple C99 cimgui+sokol starter project for Win32, Linux and macOS.
//------------------------------------------------------------------------------
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#include "sokol_imgui.h"
// custom
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
static struct {
sg_pass_action pass_action;
int value;
} state;
static void init(void) {
sg_setup(&(sg_desc){
.environment = sglue_environment(),
.logger.func = slog_func,
});
simgui_setup(&(simgui_desc_t){0});
state.value = 1;
// initial clear color
state.pass_action =
(sg_pass_action){.colors[0] = {.load_action = SG_LOADACTION_CLEAR,
.clear_value = {0.0f, 0.5f, 1.0f, 1.0}}};
}
// NON BOILERPLATE CODE
#if defined(__EMSCRIPTEN__)
void esmc_create_and_download_file(const char* filename, const char* content) {
FILE* file = fopen(filename, "w");
if (file == NULL) {
printf("Error opening file!\n");
return;
}
fprintf(file, "%s", content);
fclose(file);
// Trigger immediate download
EM_ASM({
var filename = UTF8ToString($0);
var content = FS.readFile(filename);
var blob = new Blob([content], { type: "application/octet-stream" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}, filename);
};
#else
static void native_create_and_download_file(const char* filename, const char* content){
FILE* file = fopen(filename, "w");
if (file == NULL) {
printf("Error opening file! \n");
return;
}
fprintf(file, "%s", content);
fclose(file);
printf("saved %s\n", filename);
}
#endif
void save_my_file() {
char content[100];
sprintf(content, "some content with value %d", state.value);
char filename[100];
sprintf(filename, "my_textfile_%d.txt", state.value);
#ifdef __EMSCRIPTEN__
esmc_create_and_download_file(filename, content);
#else
native_create_and_download_file(filename, content);
#endif
state.value = state.value + 1;
}
/// END
static void frame(void) {
simgui_new_frame(&(simgui_frame_desc_t){
.width = sapp_width(),
.height = sapp_height(),
.delta_time = sapp_frame_duration(),
.dpi_scale = sapp_dpi_scale(),
});
/*=== UI CODE STARTS HERE ===*/
igSetNextWindowPos((ImVec2){10, 10}, ImGuiCond_Once, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){400, 100}, ImGuiCond_Once);
igBegin("Hello Dear ImGui!", 0, ImGuiWindowFlags_None);
igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r,
ImGuiColorEditFlags_None);
// MY CUSTOM FILE SAVER BUTTON
if (igButton("file saver button", (ImVec2){150, 40})) {
save_my_file();
}
igEnd();
/*=== UI CODE ENDS HERE ===*/
sg_begin_pass(&(sg_pass){.action = state.pass_action,
.swapchain = sglue_swapchain()});
simgui_render();
sg_end_pass();
sg_commit();
}
static void cleanup(void) {
simgui_shutdown();
sg_shutdown();
}
static void event(const sapp_event* ev) { simgui_handle_event(ev); }
sapp_desc sokol_main(int argc, char* argv[]) {
(void)argc;
(void)argv;
return (sapp_desc){
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.window_title = "Hello Sokol + Dear ImGui",
.width = 800,
.height = 600,
.icon.sokol_default = true,
.logger.func = slog_func,
};
}
CMakelists.txt
# other stuff...
# Emscripten-specific linker options
if (CMAKE_SYSTEM_NAME STREQUAL Emscripten)
set(CMAKE_EXECUTABLE_SUFFIX ".html")
# use our own minimal shell.html
target_link_options(demo PRIVATE --shell-file ../sokol/shell.html)
# link with WebGL2
target_link_options(demo PRIVATE -sUSE_WEBGL2=1)
# WASM+JS size optimizations
# target_link_options(demo PRIVATE -sNO_FILESYSTEM=1 -sASSERTIONS=0 -sMALLOC=emmalloc --closure=1)
# Disable the above, put less restrictive flag
target_link_options(demo PRIVATE -sASSERTIONS=0 --closure=1)
endif()
# other stuff...
Clicking button now downloads file!
TBH, I'm surprised that this works:
if (igButton("file saver button", (ImVec2){150, 40})) {
save_my_file();
}
...you might want to check on different browsers, especially Safari, because AFAIK this JS statement:
a.click();
...might need to be called from a JS event handler (for instance from within the sokol_app.h event callback).
Hmmm... only added in an extra print statement in ... seems to be working... guess I"ll count my lucky stars for now haha.
OS: Sonoma 14.6 (23G80) Safari Version 17.6 (19618.3.11.11.5)
https://github.com/user-attachments/assets/c7d36ec1-8fa9-4df6-9b82-8d23044aba6f
demo.c
//------------------------------------------------------------------------------
// Simple C99 cimgui+sokol starter project for Win32, Linux and macOS.
//------------------------------------------------------------------------------
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#include "sokol_imgui.h"
// custom
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
static struct {
sg_pass_action pass_action;
int value;
} state;
static void init(void) {
sg_setup(&(sg_desc){
.environment = sglue_environment(),
.logger.func = slog_func,
});
simgui_setup(&(simgui_desc_t){0});
state.value = 1;
// initial clear color
state.pass_action =
(sg_pass_action){.colors[0] = {.load_action = SG_LOADACTION_CLEAR,
.clear_value = {0.0f, 0.5f, 1.0f, 1.0}}};
}
// NON BOILERPLATE CODE
#if defined(__EMSCRIPTEN__)
void esmc_create_and_download_file(const char* filename, const char* content) {
printf("emsc file save...\n");
FILE* file = fopen(filename, "w");
if (file == NULL) {
printf("Error opening file!\n");
return;
}
fprintf(file, "%s", content);
fclose(file);
// Trigger immediate download
EM_ASM({
var filename = UTF8ToString($0);
var content = FS.readFile(filename);
var blob = new Blob([content], { type: "application/octet-stream" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}, filename);
};
#else
static void native_create_and_download_file(const char* filename, const char* content){
// printf("native file save...\n");
FILE* file = fopen(filename, "w");
if (file == NULL) {
printf("Error opening file! \n");
return;
}
fprintf(file, "%s", content);
fclose(file);
printf("saved %s\n", filename);
}
#endif
void save_my_file() {
char content[100];
sprintf(content, "some content with value %d", state.value);
char filename[100];
sprintf(filename, "my_textfile_%d.txt", state.value);
#ifdef __EMSCRIPTEN__
esmc_create_and_download_file(filename, content);
#else
native_create_and_download_file(filename, content);
#endif
state.value = state.value + 1;
}
/// END
static void frame(void) {
simgui_new_frame(&(simgui_frame_desc_t){
.width = sapp_width(),
.height = sapp_height(),
.delta_time = sapp_frame_duration(),
.dpi_scale = sapp_dpi_scale(),
});
/*=== UI CODE STARTS HERE ===*/
igSetNextWindowPos((ImVec2){10, 10}, ImGuiCond_Once, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){400, 100}, ImGuiCond_Once);
igBegin("Hello Dear ImGui!", 0, ImGuiWindowFlags_None);
igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r,
ImGuiColorEditFlags_None);
// MY CUSTOM FILE SAVER BUTTON
if (igButton("file saver button", (ImVec2){150, 40})) {
printf(" button clicked...\n");
save_my_file();
}
igEnd();
/*=== UI CODE ENDS HERE ===*/
sg_begin_pass(&(sg_pass){.action = state.pass_action,
.swapchain = sglue_swapchain()});
simgui_render();
sg_end_pass();
sg_commit();
}
static void cleanup(void) {
simgui_shutdown();
sg_shutdown();
}
static void event(const sapp_event* ev) { simgui_handle_event(ev); }
sapp_desc sokol_main(int argc, char* argv[]) {
(void)argc;
(void)argv;
return (sapp_desc){
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.window_title = "Hello Sokol + Dear ImGui",
.width = 800,
.height = 600,
.icon.sokol_default = true,
.logger.func = slog_func,
};
}
Is it possible to save (download) a file when targeting web platform?
I can write to my filesystem when i compile it natively (MacOS M1)
using the starterkit... LINK: https://github.com/floooh/cimgui-sokol-starterkit
I've modified it only slightly by adding a single FILE writing function + button to trigger it
demo.c
When clicking the "File Saver Button" button I get the following error in browser
Browser is GoogleChrome Version 129.0.6668.90 (Official Build) (arm64)
Thanks!