From 581b61429116b9e661cba3d4bcb14c1057c7c878 Mon Sep 17 00:00:00 2001 From: Temdog007 <37945129+Temdog007@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:33:06 -0700 Subject: [PATCH] Emscripten: Support Custom Message Boxes (#12583) * Allow custom message boxes with colors and multiple buttons to work if Asyncify is enabled * Keep old functionality of using alert when Asyncify is not available * Update testmessage to allow for setting random colors as the color scheme of the message box --- src/video/SDL_video.c | 18 +-- src/video/emscripten/SDL_emscriptenvideo.c | 173 ++++++++++++++++++++- test/testmessage.c | 75 ++++++--- 3 files changed, 224 insertions(+), 42 deletions(-) diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 3b2c9e261..fd1a84ae6 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -5724,23 +5724,7 @@ bool SDL_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID) bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags flags, const char *title, const char *message, SDL_Window *window) { -#ifdef SDL_PLATFORM_EMSCRIPTEN - // !!! FIXME: propose a browser API for this, get this #ifdef out of here? - /* Web browsers don't (currently) have an API for a custom message box - that can block, but for the most common case (SDL_ShowSimpleMessageBox), - we can use the standard Javascript alert() function. */ - if (!title) { - title = ""; - } - if (!message) { - message = ""; - } - EM_ASM({ - alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); - }, - title, message); - return true; -#elif defined(SDL_PLATFORM_3DS) +#if defined(SDL_PLATFORM_3DS) errorConf errCnf; bool hasGpuRight; diff --git a/src/video/emscripten/SDL_emscriptenvideo.c b/src/video/emscripten/SDL_emscriptenvideo.c index ad49e2fa1..52389f1c1 100644 --- a/src/video/emscripten/SDL_emscriptenvideo.c +++ b/src/video/emscripten/SDL_emscriptenvideo.c @@ -192,10 +192,181 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void) return device; } +static bool Emscripten_ShowMessagebox(const SDL_MessageBoxData *messageboxdata, int *buttonID) { + if (emscripten_has_asyncify() && SDL_GetHintBoolean(SDL_HINT_EMSCRIPTEN_ASYNCIFY, true)) { + char dialog_background[32]; + char dialog_color[32]; + char button_border[32]; + char button_background[32]; + char button_hovered[32]; + + if (messageboxdata->colorScheme) { + SDL_MessageBoxColor color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BACKGROUND]; + SDL_snprintf(dialog_background, sizeof(dialog_background), "rgb(%u, %u, %u)", color.r, color.g, color.b); + + color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_TEXT]; + SDL_snprintf(dialog_color, sizeof(dialog_color), "rgb(%u, %u, %u)", color.r, color.g, color.b); + + color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_BORDER]; + SDL_snprintf(button_border, sizeof(button_border), "rgb(%u, %u, %u)", color.r, color.g, color.b); + + color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_BACKGROUND]; + SDL_snprintf(button_background, sizeof(button_background), "rgb(%u, %u, %u)", color.r, color.g, color.b); + + color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_SELECTED]; + SDL_snprintf(button_hovered, sizeof(button_hovered), "rgb(%u, %u, %u)", color.r, color.g, color.b); + } else { + SDL_zero(dialog_background); + SDL_zero(dialog_color); + SDL_zero(button_border); + SDL_zero(button_background); + SDL_zero(button_hovered); + } + + // TODO: Handle parent window when multiple windows can be added in Emscripten builds + char dialog_id[64]; + SDL_snprintf(dialog_id, sizeof(dialog_id), "SDL3_messagebox_%u", SDL_rand_bits()); + EM_ASM({ + var title = UTF8ToString($0); + var message = UTF8ToString($1); + var background = UTF8ToString($2); + var color = UTF8ToString($3); + var id = UTF8ToString($4); + + // Dialogs are always put in the front of the DOM + var dialog = document.createElement("dialog"); + // Set class to allow for CSS selectors + dialog.classList.add("SDL3_messagebox"); + dialog.id = id; + dialog.style.color = color; + dialog.style.backgroundColor = background; + document.body.append(dialog); + + var h1 = document.createElement("h1"); + h1.innerText = title; + dialog.append(h1); + + var p = document.createElement("p"); + p.innerText = message; + dialog.append(p); + + dialog.showModal(); + }, messageboxdata->title, messageboxdata->message, dialog_background, dialog_color, dialog_id); + + int i; + for (i = 0; i < messageboxdata->numbuttons; ++i) { + SDL_MessageBoxButtonData button = messageboxdata->buttons[i]; + + const int created = EM_ASM_INT({ + var dialog_id = UTF8ToString($0); + var text = UTF8ToString($1); + var responseId = $2; + var clickOnReturn = $3; + var clickOnEscape = $4; + var border = UTF8ToString($5); + var background = UTF8ToString($6); + var hovered = UTF8ToString($7); + + var dialog = document.getElementById(dialog_id); + if (!dialog) { + return false; + } + + var button = document.createElement("button"); + button.innerText = text; + button.style.borderColor = border; + button.style.backgroundColor = background; + + dialog.addEventListener('keydown', function(e) { + if (clickOnReturn && e.key === "Enter") { + e.preventDefault(); + button.click(); + } else if (clickOnEscape && e.key === "Escape") { + e.preventDefault(); + button.click(); + } + }); + dialog.addEventListener('cancel', function(e){ + e.preventDefault(); + }); + + button.onmouseenter = function(e){ + button.style.backgroundColor = hovered; + }; + button.onmouseleave = function(e){ + button.style.backgroundColor = background; + }; + button.onclick = function(e) { + dialog.close(responseId); + }; + + dialog.append(button); + return true; + }, + dialog_id, + button.text, + button.buttonID, + button.flags & SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + button.flags & SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + button_border, + button_background, + button_hovered + ); + + if (!created) { + return false; + } + } + + while (true) { + // give back control to browser for screen refresh + emscripten_sleep(0); + + const int dialog_open = EM_ASM_INT({ + var dialog_id = UTF8ToString($0); + + var dialog = document.getElementById(dialog_id); + if (!dialog) { + return false; + } + return dialog.open; + }, dialog_id); + + if (dialog_open) { + continue; + } + + *buttonID = EM_ASM_INT({ + var dialog_id = UTF8ToString($0); + var dialog = document.getElementById(dialog_id); + if (!dialog) { + return 0; + } + try + { + return parseInt(dialog.returnValue); + } + catch(e) + { + return 0; + } + }, dialog_id); + break; + } + + } else { + // Cannot add elements to DOM and block without Asyncify. So, fall back to the alert function. + EM_ASM({ + alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); + }, messageboxdata->title, messageboxdata->message); + } + return true; +} + VideoBootStrap Emscripten_bootstrap = { EMSCRIPTENVID_DRIVER_NAME, "SDL emscripten video driver", Emscripten_CreateDevice, - NULL, // no ShowMessageBox implementation + Emscripten_ShowMessagebox, false }; diff --git a/test/testmessage.c b/test/testmessage.c index 1e5dbc9ab..138711952 100644 --- a/test/testmessage.c +++ b/test/testmessage.c @@ -36,6 +36,7 @@ quit(int rc) static int SDLCALL button_messagebox(void *eventNumber) { + int i; const SDL_MessageBoxButtonData buttons[] = { { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, @@ -43,52 +44,78 @@ button_messagebox(void *eventNumber) { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 1, "Cancel" }, + { 0, + 2, + "Retry" } }; - SDL_MessageBoxData data = { SDL_MESSAGEBOX_INFORMATION, NULL, /* no parent window */ "Custom MessageBox", "This is a custom messagebox", - 2, + sizeof(buttons) / sizeof(SDL_MessageBoxButtonData), NULL, /* buttons */ NULL /* Default color scheme */ }; - int button = -1; - int success = 0; - data.buttons = buttons; - if (eventNumber) { - data.message = "This is a custom messagebox from a background thread."; - } + for (i = 0; ; ++i) { + SDL_MessageBoxColorScheme colorScheme; + if (i != 0) { + int j; + for (j = 0; j < SDL_MESSAGEBOX_COLOR_COUNT; ++j) { + colorScheme.colors[j].r = SDL_rand(256); + colorScheme.colors[j].g = SDL_rand(256); + colorScheme.colors[j].b = SDL_rand(256); + } + data.colorScheme = &colorScheme; + } else { + data.colorScheme = NULL; + } + + int button = -1; + data.buttons = buttons; + if (eventNumber) { + data.message = "This is a custom messagebox from a background thread."; + } + + if (!SDL_ShowMessageBox(&data, &button)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error Presenting MessageBox: %s", SDL_GetError()); + if (eventNumber) { + SDL_Event event; + event.type = (Uint32)(intptr_t)eventNumber; + SDL_PushEvent(&event); + return 1; + } else { + quit(2); + } + } + + const char* text; + if (button == 1) { + text = "Cancel"; + } else if (button == 2) { + text = "Retry"; + } else { + text = "OK"; + } + SDL_Log("Pressed button: %d, %s", button, button == -1 ? "[closed]" : text); - success = SDL_ShowMessageBox(&data, &button); - if (success == -1) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error Presenting MessageBox: %s", SDL_GetError()); if (eventNumber) { SDL_Event event; event.type = (Uint32)(intptr_t)eventNumber; SDL_PushEvent(&event); - return 1; - } else { - quit(2); } - } - SDL_Log("Pressed button: %d, %s", button, button == -1 ? "[closed]" : button == 1 ? "Cancel" - : "OK"); - if (eventNumber) { - SDL_Event event; - event.type = (Uint32)(intptr_t)eventNumber; - SDL_PushEvent(&event); + if (button == 2) { + continue; + } + return 0; } - - return 0; } int main(int argc, char *argv[]) { - int success; + bool success; SDLTest_CommonState *state; /* Initialize test framework */