Atlas - SDL_emscriptenvideo.c
Home / ext / SDL / src / video / emscripten Lines: 3 | Size: 30604 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "SDL_internal.h" 22 23#ifdef SDL_VIDEO_DRIVER_EMSCRIPTEN 24 25#include "../SDL_sysvideo.h" 26#include "../SDL_pixels_c.h" 27#include "../../events/SDL_events_c.h" 28#include "../../SDL_hints_c.h" 29 30#include "SDL_emscriptenvideo.h" 31#include "SDL_emscriptenopengles.h" 32#include "SDL_emscriptenframebuffer.h" 33#include "SDL_emscriptenevents.h" 34#include "SDL_emscriptenmouse.h" 35 36#define EMSCRIPTENVID_DRIVER_NAME "emscripten" 37 38// Initialization/Query functions 39static bool Emscripten_VideoInit(SDL_VideoDevice *_this); 40static bool Emscripten_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode); 41static void Emscripten_VideoQuit(SDL_VideoDevice *_this); 42static bool Emscripten_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); 43 44static bool Emscripten_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); 45static void Emscripten_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window); 46static void Emscripten_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window * window, bool resizable); 47static void Emscripten_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h); 48static void Emscripten_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); 49static SDL_FullscreenResult Emscripten_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen); 50static void Emscripten_PumpEvents(SDL_VideoDevice *_this); 51static void Emscripten_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); 52static bool Emscripten_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon); 53static bool Emscripten_SetWindowFillDocument(SDL_VideoDevice *_this, SDL_Window *window, bool fill); 54 55SDL_Window *Emscripten_fill_document_window = NULL; 56 57static bool pumpevents_has_run = false; 58static int pending_swap_interval = -1; 59 60// Emscripten driver bootstrap functions 61 62static void Emscripten_DeleteDevice(SDL_VideoDevice *device) 63{ 64 SDL_free(device); 65} 66 67static SDL_SystemTheme Emscripten_GetSystemTheme(void) 68{ 69 /* Technically, light theme can mean explicit light theme or no preference. 70 https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme#syntax */ 71 72 int theme_code = MAIN_THREAD_EM_ASM_INT({ 73 if (!window.matchMedia) { 74 return -1; 75 } 76 77 if (window.matchMedia('(prefers-color-scheme: light)').matches) { 78 return 0; 79 } 80 81 if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 82 return 1; 83 } 84 85 return -1; 86 }); 87 88 switch (theme_code) { 89 case 0: 90 return SDL_SYSTEM_THEME_LIGHT; 91 92 case 1: 93 return SDL_SYSTEM_THEME_DARK; 94 95 default: 96 return SDL_SYSTEM_THEME_UNKNOWN; 97 } 98} 99 100static void Emscripten_ListenSystemTheme(void) 101{ 102 MAIN_THREAD_EM_ASM({ 103 if (window.matchMedia) { 104 var SDL3 = Module['SDL3']; 105 106 SDL3.eventHandlerThemeChanged = function(event) { 107 _Emscripten_SendSystemThemeChangedEvent(); 108 }; 109 110 SDL3.themeChangedMatchMedia = window.matchMedia('(prefers-color-scheme: dark)'); 111 SDL3.themeChangedMatchMedia.addEventListener('change', SDL3.eventHandlerThemeChanged); 112 } 113 }); 114} 115 116static void Emscripten_UnlistenSystemTheme(void) 117{ 118 MAIN_THREAD_EM_ASM({ 119 if (typeof(Module['SDL3']) !== 'undefined') { 120 var SDL3 = Module['SDL3']; 121 122 SDL3.themeChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerThemeChanged); 123 SDL3.themeChangedMatchMedia = undefined; 124 SDL3.eventHandlerThemeChanged = undefined; 125 } 126 }); 127} 128 129EMSCRIPTEN_KEEPALIVE void Emscripten_SendSystemThemeChangedEvent(void) 130{ 131 SDL_SetSystemTheme(Emscripten_GetSystemTheme()); 132} 133 134static SDL_VideoDevice *Emscripten_CreateDevice(void) 135{ 136 SDL_VideoDevice *device; 137 138 // Initialize all variables that we clean on shutdown 139 device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); 140 if (!device) { 141 return NULL; 142 } 143 144 /* Firefox sends blur event which would otherwise prevent full screen 145 * when the user clicks to allow full screen. 146 * See https://bugzilla.mozilla.org/show_bug.cgi?id=1144964 147 */ 148 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); 149 150 // Set the function pointers 151 device->VideoInit = Emscripten_VideoInit; 152 device->VideoQuit = Emscripten_VideoQuit; 153 device->GetDisplayUsableBounds = Emscripten_GetDisplayUsableBounds; 154 device->SetDisplayMode = Emscripten_SetDisplayMode; 155 156 device->PumpEvents = Emscripten_PumpEvents; 157 158 device->CreateSDLWindow = Emscripten_CreateWindow; 159 device->SetWindowTitle = Emscripten_SetWindowTitle; 160 device->SetWindowIcon = Emscripten_SetWindowIcon; 161 /*device->SetWindowPosition = Emscripten_SetWindowPosition;*/ 162 device->SetWindowSize = Emscripten_SetWindowSize; 163 device->SetWindowResizable = Emscripten_SetWindowResizable; 164 /*device->ShowWindow = Emscripten_ShowWindow; 165 device->HideWindow = Emscripten_HideWindow; 166 device->RaiseWindow = Emscripten_RaiseWindow; 167 device->MaximizeWindow = Emscripten_MaximizeWindow; 168 device->MinimizeWindow = Emscripten_MinimizeWindow; 169 device->RestoreWindow = Emscripten_RestoreWindow; 170 device->SetWindowMouseGrab = Emscripten_SetWindowMouseGrab;*/ 171 device->GetWindowSizeInPixels = Emscripten_GetWindowSizeInPixels; 172 device->DestroyWindow = Emscripten_DestroyWindow; 173 device->SetWindowFullscreen = Emscripten_SetWindowFullscreen; 174 device->SetWindowFillDocument = Emscripten_SetWindowFillDocument; 175 176 device->CreateWindowFramebuffer = Emscripten_CreateWindowFramebuffer; 177 device->UpdateWindowFramebuffer = Emscripten_UpdateWindowFramebuffer; 178 device->DestroyWindowFramebuffer = Emscripten_DestroyWindowFramebuffer; 179 180 device->GL_LoadLibrary = Emscripten_GLES_LoadLibrary; 181 device->GL_GetProcAddress = Emscripten_GLES_GetProcAddress; 182 device->GL_UnloadLibrary = Emscripten_GLES_UnloadLibrary; 183 device->GL_CreateContext = Emscripten_GLES_CreateContext; 184 device->GL_MakeCurrent = Emscripten_GLES_MakeCurrent; 185 device->GL_SetSwapInterval = Emscripten_GLES_SetSwapInterval; 186 device->GL_GetSwapInterval = Emscripten_GLES_GetSwapInterval; 187 device->GL_SwapWindow = Emscripten_GLES_SwapWindow; 188 device->GL_DestroyContext = Emscripten_GLES_DestroyContext; 189 190 device->free = Emscripten_DeleteDevice; 191 192 Emscripten_ListenSystemTheme(); 193 device->system_theme = Emscripten_GetSystemTheme(); 194 195 return device; 196} 197 198static bool Emscripten_ShowMessagebox(const SDL_MessageBoxData *messageboxdata, int *buttonID) { 199 if (emscripten_has_asyncify() && SDL_GetHintBoolean(SDL_HINT_EMSCRIPTEN_ASYNCIFY, true)) { 200 char dialog_background[32]; 201 char dialog_color[32]; 202 char button_border[32]; 203 char button_background[32]; 204 char button_hovered[32]; 205 206 if (messageboxdata->colorScheme) { 207 SDL_MessageBoxColor color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BACKGROUND]; 208 SDL_snprintf(dialog_background, sizeof(dialog_background), "rgb(%u, %u, %u)", color.r, color.g, color.b); 209 210 color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_TEXT]; 211 SDL_snprintf(dialog_color, sizeof(dialog_color), "rgb(%u, %u, %u)", color.r, color.g, color.b); 212 213 color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_BORDER]; 214 SDL_snprintf(button_border, sizeof(button_border), "rgb(%u, %u, %u)", color.r, color.g, color.b); 215 216 color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_BACKGROUND]; 217 SDL_snprintf(button_background, sizeof(button_background), "rgb(%u, %u, %u)", color.r, color.g, color.b); 218 219 color = messageboxdata->colorScheme->colors[SDL_MESSAGEBOX_COLOR_BUTTON_SELECTED]; 220 SDL_snprintf(button_hovered, sizeof(button_hovered), "rgb(%u, %u, %u)", color.r, color.g, color.b); 221 } else { 222 SDL_zero(dialog_background); 223 SDL_zero(dialog_color); 224 SDL_zero(button_border); 225 SDL_zero(button_background); 226 SDL_zero(button_hovered); 227 } 228 229 // TODO: Handle parent window when multiple windows can be added in Emscripten builds 230 char dialog_id[64]; 231 SDL_snprintf(dialog_id, sizeof(dialog_id), "SDL3_messagebox_%u", SDL_rand_bits()); 232 EM_ASM({ 233 var title = UTF8ToString($0); 234 var message = UTF8ToString($1); 235 var background = UTF8ToString($2); 236 var color = UTF8ToString($3); 237 var id = UTF8ToString($4); 238 239 // Dialogs are always put in the front of the DOM 240 var dialog = document.createElement("dialog"); 241 // Set class to allow for CSS selectors 242 dialog.classList.add("SDL3_messagebox"); 243 dialog.id = id; 244 dialog.style.color = color; 245 dialog.style.backgroundColor = background; 246 document.body.append(dialog); 247 248 var h1 = document.createElement("h1"); 249 h1.innerText = title; 250 dialog.append(h1); 251 252 var p = document.createElement("p"); 253 p.innerText = message; 254 dialog.append(p); 255 256 dialog.showModal(); 257 }, messageboxdata->title, messageboxdata->message, dialog_background, dialog_color, dialog_id); 258 259 int i; 260 for (i = 0; i < messageboxdata->numbuttons; ++i) { 261 SDL_MessageBoxButtonData button = messageboxdata->buttons[i]; 262 263 const int created = EM_ASM_INT({ 264 var dialog_id = UTF8ToString($0); 265 var text = UTF8ToString($1); 266 var responseId = $2; 267 var clickOnReturn = $3; 268 var clickOnEscape = $4; 269 var border = UTF8ToString($5); 270 var background = UTF8ToString($6); 271 var hovered = UTF8ToString($7); 272 273 var dialog = document.getElementById(dialog_id); 274 if (!dialog) { 275 return false; 276 } 277 278 var button = document.createElement("button"); 279 button.innerText = text; 280 button.style.borderColor = border; 281 button.style.backgroundColor = background; 282 283 dialog.addEventListener('keydown', function(e) { 284 if (clickOnReturn && e.key === "Enter") { 285 e.preventDefault(); 286 button.click(); 287 } else if (clickOnEscape && e.key === "Escape") { 288 e.preventDefault(); 289 button.click(); 290 } 291 }); 292 dialog.addEventListener('cancel', function(e){ 293 e.preventDefault(); 294 }); 295 296 button.onmouseenter = function(e){ 297 button.style.backgroundColor = hovered; 298 }; 299 button.onmouseleave = function(e){ 300 button.style.backgroundColor = background; 301 }; 302 button.onclick = function(e) { 303 dialog.close(responseId); 304 }; 305 306 dialog.append(button); 307 return true; 308 }, 309 dialog_id, 310 button.text, 311 button.buttonID, 312 button.flags & SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 313 button.flags & SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 314 button_border, 315 button_background, 316 button_hovered 317 ); 318 319 if (!created) { 320 return false; 321 } 322 } 323 324 while (true) { 325 // give back control to browser for screen refresh 326 emscripten_sleep(0); 327 328 const int dialog_open = EM_ASM_INT({ 329 var dialog_id = UTF8ToString($0); 330 331 var dialog = document.getElementById(dialog_id); 332 if (!dialog) { 333 return false; 334 } 335 return dialog.open; 336 }, dialog_id); 337 338 if (dialog_open) { 339 continue; 340 } 341 342 *buttonID = EM_ASM_INT({ 343 var dialog_id = UTF8ToString($0); 344 var dialog = document.getElementById(dialog_id); 345 if (!dialog) { 346 return 0; 347 } 348 try 349 { 350 return parseInt(dialog.returnValue); 351 } 352 catch(e) 353 { 354 return 0; 355 } 356 }, dialog_id); 357 break; 358 } 359 360 } else { 361 // Cannot add elements to DOM and block without Asyncify. So, fall back to the alert function. 362 EM_ASM({ 363 alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); 364 }, messageboxdata->title, messageboxdata->message); 365 } 366 return true; 367} 368 369VideoBootStrap Emscripten_bootstrap = { 370 EMSCRIPTENVID_DRIVER_NAME, "SDL emscripten video driver", 371 Emscripten_CreateDevice, 372 Emscripten_ShowMessagebox, 373 false 374}; 375 376bool Emscripten_VideoInit(SDL_VideoDevice *_this) 377{ 378 SDL_DisplayMode mode; 379 380 // Use a fake 32-bpp desktop mode 381 SDL_zero(mode); 382 mode.format = SDL_PIXELFORMAT_RGBA32; 383 emscripten_get_screen_size(&mode.w, &mode.h); 384 mode.pixel_density = emscripten_get_device_pixel_ratio(); 385 386 if (SDL_AddBasicVideoDisplay(&mode) == 0) { 387 return false; 388 } 389 390 Emscripten_InitMouse(); 391 392 // Assume we have a mouse and keyboard 393 SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL); 394 SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL); 395 396 Emscripten_RegisterGlobalEventHandlers(_this); 397 398 // We're done! 399 return true; 400} 401 402static bool Emscripten_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode) 403{ 404 // can't do this 405 return true; 406} 407 408static void Emscripten_VideoQuit(SDL_VideoDevice *_this) 409{ 410 Emscripten_UnregisterGlobalEventHandlers(_this); 411 Emscripten_QuitMouse(); 412 Emscripten_UnlistenSystemTheme(); 413 pumpevents_has_run = false; 414 pending_swap_interval = -1; 415} 416 417static bool Emscripten_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect) 418{ 419 if (rect) { 420 rect->x = 0; 421 rect->y = 0; 422 rect->w = MAIN_THREAD_EM_ASM_INT({ 423 return window.innerWidth; 424 }); 425 rect->h = MAIN_THREAD_EM_ASM_INT({ 426 return window.innerHeight; 427 }); 428 } 429 return true; 430} 431 432bool Emscripten_ShouldSetSwapInterval(int interval) 433{ 434 if (!pumpevents_has_run) { 435 pending_swap_interval = interval; 436 return false; 437 } 438 return true; 439} 440 441static void Emscripten_PumpEvents(SDL_VideoDevice *_this) 442{ 443 if (!pumpevents_has_run) { 444 // we assume you've set a mainloop by the time you've called pumpevents, so we delay initial SetInterval changes until then. 445 // otherwise you'll get a warning on the javascript console. 446 pumpevents_has_run = true; 447 if (pending_swap_interval >= 0) { 448 Emscripten_GLES_SetSwapInterval(_this, pending_swap_interval); 449 pending_swap_interval = -1; 450 } 451 } 452} 453 454EMSCRIPTEN_KEEPALIVE void requestFullscreenThroughSDL(SDL_Window *window) 455{ 456 SDL_SetWindowFullscreen(window, true); 457} 458 459static bool Emscripten_SetWindowFillDocument(SDL_VideoDevice *_this, SDL_Window *window, bool fill) 460{ 461 SDL_WindowData *wdata = window->internal; 462 463 if (fill && Emscripten_fill_document_window && (Emscripten_fill_document_window != window)) { 464 return SDL_SetError("Only one fill-document window allowed at a time."); 465 } 466 467 // fill_document takes up the entire page and resizes as the browser window resizes. 468 if (fill) { 469 Emscripten_fill_document_window = window; 470 471 const int w = MAIN_THREAD_EM_ASM_INT({ return window.innerWidth; }); 472 const int h = MAIN_THREAD_EM_ASM_INT({ return window.innerHeight; }); 473 const double scaled_w = w * wdata->pixel_ratio; 474 const double scaled_h = h * wdata->pixel_ratio; 475 476 wdata->non_fill_document_width = window->w; 477 wdata->non_fill_document_height = window->h; 478 479 MAIN_THREAD_EM_ASM({ 480 var canvas = document.querySelector(UTF8ToString($0)); 481 canvas.SDL3_original_position = canvas.style.position; 482 canvas.SDL3_original_top = canvas.style.top; 483 canvas.SDL3_original_left = canvas.style.left; 484 485 // hide everything on the page that isn't the canvas. 486 var div = document.createElement('div'); 487 div.id = 'SDL3_fill_document_background_elements'; 488 489 div.SDL3_canvas = canvas; 490 div.SDL3_canvas_parent = canvas.parentNode; 491 div.SDL3_canvas_nextsib = canvas.nextSibling; 492 493 var children = Array.from(document.body.children); 494 for (var child of children) { 495 div.appendChild(child); 496 } 497 document.body.appendChild(div); 498 div.style.display = 'none'; 499 document.body.appendChild(canvas); 500 canvas.style.position = 'fixed'; 501 canvas.style.top = '0'; 502 canvas.style.left = '0'; 503 }, wdata->canvas_id); 504 505 emscripten_set_canvas_element_size(wdata->canvas_id, SDL_lroundf(scaled_w), SDL_lroundf(scaled_h)); 506 507 // set_canvas_size unsets this 508 if (wdata->pixel_ratio != 1.0f) { 509 emscripten_set_element_css_size(wdata->canvas_id, w, h); 510 } 511 512 // force the event to trigger, so pixel ratio changes can be handled 513 window->w = window->h = 0; 514 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, SDL_lroundf(w), SDL_lroundf(h)); 515 } else { 516 const bool transitioning = (Emscripten_fill_document_window == window); 517 if (transitioning) { 518 MAIN_THREAD_EM_ASM({ 519 // if we had previously hidden everything behind a fill_document window, put it back. 520 var div = document.getElementById('SDL3_fill_document_background_elements'); 521 if (div) { 522 if (div.SDL3_canvas_nextsib) { 523 div.SDL3_canvas_parent.insertBefore(div.SDL3_canvas, div.SDL3_canvas_nextsib); 524 } else { 525 div.SDL3_canvas_parent.appendChild(div.SDL3_canvas); 526 } 527 while (div.firstChild) { 528 document.body.insertBefore(div.firstChild, div); 529 } 530 div.SDL3_canvas.style.position = div.SDL3_canvas.SDL3_original_position; 531 div.SDL3_canvas.style.top = div.SDL3_canvas.SDL3_original_top; 532 div.SDL3_canvas.style.left = div.SDL3_canvas.SDL3_original_left; 533 div.remove(); 534 } 535 }); 536 Emscripten_fill_document_window = NULL; 537 } 538 539 window->w = wdata->non_fill_document_width; 540 window->h = wdata->non_fill_document_height; 541 const double scaled_w = SDL_floor(window->w * wdata->pixel_ratio); 542 const double scaled_h = SDL_floor(window->h * wdata->pixel_ratio); 543 emscripten_set_canvas_element_size(wdata->canvas_id, SDL_lroundf(scaled_w), SDL_lroundf(scaled_h)); 544 545 // if the size is not being controlled by css, we need to scale down for hidpi 546 if (!wdata->external_size && (wdata->pixel_ratio != 1.0f)) { 547 // scale canvas down 548 emscripten_set_element_css_size(wdata->canvas_id, window->w, window->h); 549 } 550 551 if (transitioning) { 552 window->w = window->h = 0; 553 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, wdata->non_fill_document_width, wdata->non_fill_document_height); 554 } 555 } 556 557 return true; 558} 559 560static bool Emscripten_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) 561{ 562 SDL_WindowData *wdata; 563 double css_w, css_h; 564 const char *selector; 565 566 bool fill_document = ((window->flags & SDL_WINDOW_FILL_DOCUMENT) != 0); 567 if (fill_document && Emscripten_fill_document_window) { 568 fill_document = false; // only one allowed at a time. 569 window->flags &= ~SDL_WINDOW_FILL_DOCUMENT; // !!! FIXME: should this fail instead? 570 } 571 572 // Allocate window internal data 573 wdata = (SDL_WindowData *)SDL_calloc(1, sizeof(SDL_WindowData)); 574 if (!wdata) { 575 return false; 576 } 577 578 selector = SDL_GetHint(SDL_HINT_EMSCRIPTEN_CANVAS_SELECTOR); 579 if (!selector || !*selector) { 580 selector = SDL_GetStringProperty(props, SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_CANVAS_ID_STRING, "#canvas"); 581 } 582 wdata->canvas_id = SDL_strdup(selector); 583 584 selector = SDL_GetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT); 585 if (!selector || !*selector) { 586 selector = SDL_GetStringProperty(props, SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING, "#window"); 587 } 588 wdata->keyboard_element = SDL_strdup(selector); 589 590 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { 591 wdata->pixel_ratio = emscripten_get_device_pixel_ratio(); 592 } else { 593 wdata->pixel_ratio = 1.0f; 594 } 595 596 // set a fake size to check if there is any CSS sizing the canvas 597 emscripten_set_canvas_element_size(wdata->canvas_id, 1, 1); 598 emscripten_get_element_css_size(wdata->canvas_id, &css_w, &css_h); 599 600 wdata->external_size = SDL_floor(css_w) != 1 || SDL_floor(css_h) != 1; 601 if (wdata->external_size) { 602 fill_document = false; // can't be resizable if something else is controlling it. 603 window->w = (int) css_w; 604 window->h = (int) css_h; 605 } 606 607 wdata->window = window; 608 609 // Setup driver data for this window 610 window->internal = wdata; 611 612 wdata->non_fill_document_width = window->w; 613 wdata->non_fill_document_height = window->h; 614 Emscripten_SetWindowFillDocument(_this, window, fill_document); 615 616 // One window, it always has focus 617 SDL_SetMouseFocus(window); 618 SDL_SetKeyboardFocus(window); 619 620 Emscripten_RegisterEventHandlers(wdata); 621 622 // Make the emscripten "fullscreen" button go through SDL. 623 MAIN_THREAD_EM_ASM({ 624 Module['requestFullscreen'] = function(lockPointer, resizeCanvas) { 625 _requestFullscreenThroughSDL($0); 626 }; 627 }, window); 628 629 // Ensure various things are added to the window's properties 630 SDL_SetStringProperty(window->props, SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING, wdata->canvas_id); 631 SDL_SetStringProperty(window->props, SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING, wdata->keyboard_element); 632 633 // Window has been successfully created 634 return true; 635} 636 637static void Emscripten_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window * window, bool resizable) 638{ 639 // this function just has to exist or the higher level won't let the window change its SDL_WINDOW_RESIZABLE flag. 640} 641 642static void Emscripten_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) 643{ 644 if (window->internal) { 645 SDL_WindowData *data = window->internal; 646 if (window == Emscripten_fill_document_window) { 647 return; // canvas size is being dictated by the browser window size, refuse request. 648 } 649 650 // update pixel ratio 651 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { 652 data->pixel_ratio = emscripten_get_device_pixel_ratio(); 653 } 654 emscripten_set_canvas_element_size(data->canvas_id, SDL_lroundf(window->pending.w * data->pixel_ratio), SDL_lroundf(window->pending.h * data->pixel_ratio)); 655 656 // scale canvas down 657 if (!data->external_size && data->pixel_ratio != 1.0f) { 658 emscripten_set_element_css_size(data->canvas_id, window->pending.w, window->pending.h); 659 } 660 661 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, window->pending.w, window->pending.h); 662 } 663} 664 665static void Emscripten_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h) 666{ 667 SDL_WindowData *data; 668 if (window->internal) { 669 data = window->internal; 670 *w = SDL_lroundf(window->w * data->pixel_ratio); 671 *h = SDL_lroundf(window->h * data->pixel_ratio); 672 } 673} 674 675static void Emscripten_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) 676{ 677 SDL_WindowData *data; 678 679 Emscripten_SetWindowFillDocument(_this, window, false); 680 681 if (window->internal) { 682 data = window->internal; 683 684 Emscripten_UnregisterEventHandlers(data); 685 686 // We can't destroy the canvas, so resize it to zero instead 687 emscripten_set_canvas_element_size(data->canvas_id, 0, 0); 688 if (data->pixel_ratio != 1.0f) { 689 emscripten_set_element_css_size(data->canvas_id, 1, 1); 690 } 691 SDL_free(data->canvas_id); 692 693 SDL_free(data->keyboard_element); 694 695 SDL_free(window->internal); 696 window->internal = NULL; 697 } 698 699 MAIN_THREAD_EM_ASM({ 700 // just ignore clicks on the fullscreen button while there's no SDL window. 701 Module['requestFullscreen'] = function(lockPointer, resizeCanvas) {}; 702 }); 703} 704 705static SDL_FullscreenResult Emscripten_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) 706{ 707 SDL_WindowData *data; 708 int res = -1; 709 710 if (window->internal) { 711 data = window->internal; 712 713 if (data->fullscreen_change_in_progress) { 714 return SDL_FULLSCREEN_FAILED;; 715 } 716 717 EmscriptenFullscreenChangeEvent fsevent; 718 if (emscripten_get_fullscreen_status(&fsevent) == EMSCRIPTEN_RESULT_SUCCESS) { 719 if ((fullscreen == SDL_FULLSCREEN_OP_ENTER) == fsevent.isFullscreen) { 720 return SDL_FULLSCREEN_SUCCEEDED; // already there. 721 } 722 } 723 724 if (fullscreen) { 725 EmscriptenFullscreenStrategy strategy; 726 bool is_fullscreen_desktop = !window->fullscreen_exclusive; 727 728 SDL_zero(strategy); 729 strategy.scaleMode = is_fullscreen_desktop ? EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH : EMSCRIPTEN_FULLSCREEN_SCALE_ASPECT; 730 731 if (!is_fullscreen_desktop) { 732 strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_NONE; 733 } else if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { 734 strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF; 735 } else { 736 strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; 737 } 738 739 strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; 740 741 strategy.canvasResizedCallback = Emscripten_HandleCanvasResize; 742 strategy.canvasResizedCallbackUserData = data; 743 744 data->fullscreen_mode_flags = (window->flags & SDL_WINDOW_FULLSCREEN); 745 data->fullscreen_resize = is_fullscreen_desktop; 746 747 res = emscripten_request_fullscreen_strategy(data->canvas_id, 1, &strategy); 748 } else { 749 res = emscripten_exit_fullscreen(); 750 } 751 } 752 753 if (res == EMSCRIPTEN_RESULT_SUCCESS) { 754 data->fullscreen_change_in_progress = true; // even on success, this might animate to the new state. 755 return SDL_FULLSCREEN_SUCCEEDED; 756 } else if (res == EMSCRIPTEN_RESULT_DEFERRED) { 757 data->fullscreen_change_in_progress = true; 758 return SDL_FULLSCREEN_PENDING; 759 } else { 760 return SDL_FULLSCREEN_FAILED; 761 } 762} 763 764static void Emscripten_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) 765{ 766 emscripten_set_window_title(window->title); 767} 768 769static bool Emscripten_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon) 770{ 771 // Create dynamic memory stream for PNG encoding 772 SDL_IOStream *stream = SDL_IOFromDynamicMem(); 773 if (!stream) { 774 return false; 775 } 776 777 // Encode icon to PNG using SDL's native PNG encoder 778 if (!SDL_SavePNG_IO(icon, stream, false)) { 779 SDL_CloseIO(stream); 780 return false; 781 } 782 783 // Get the PNG data from the stream 784 void *png_data = SDL_GetPointerProperty( 785 SDL_GetIOProperties(stream), 786 SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, 787 NULL 788 ); 789 size_t png_size = (size_t)SDL_GetIOSize(stream); 790 791 // Pass PNG data to JavaScript 792 MAIN_THREAD_EM_ASM({ 793 // Use `.slice` to make a copy of the data if we are dealing with a SharedArrayBuffer, or `.subarray` to create a 794 // view into the existing buffer if its non-shared. 795 var pngData = HEAPU8.buffer instanceof ArrayBuffer ? HEAPU8.subarray($0, $0 + $1) : HEAPU8.slice($0, $0 + $1); 796 797 var blob = new Blob([pngData], {type: 'image/png'}); 798 var url = URL.createObjectURL(blob); 799 800 // Set as favicon (create link element if it doesn't exist) 801 var link = document.querySelector("link[rel~='icon']"); 802 if (!link) { 803 link = document.createElement('link'); 804 link.rel = 'icon'; 805 link.type = 'image/png'; 806 document.head.appendChild(link); 807 } 808 809 // Revoke old URL if it exists to prevent memory leaks 810 if (link.href && link.href.startsWith('blob:')) { 811 URL.revokeObjectURL(link.href); 812 } 813 814 link.href = url; 815 }, png_data, png_size); 816 817 SDL_CloseIO(stream); 818 return true; 819} 820 821#endif // SDL_VIDEO_DRIVER_EMSCRIPTEN 822[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.