From 581b61429116b9e661cba3d4bcb14c1057c7c878 Mon Sep 17 00:00:00 2001
From: Temdog007 <[EMAIL REDACTED]>
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 3b2c9e26182a7..fd1a84ae64081 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 ad49e2fa1a137..52389f1c1d7a4 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 1e5dbc9ab8499..1387119523419 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 */