From 3c4fc605b49afcdc2b4deaf560f4d68fde4e6721 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Sun, 25 Apr 2021 10:55:07 +1000 Subject: [PATCH 1/3] Web: Move all nasty JavaScript-in-C code to a separate dedicated file --- src/Game.c | 11 +- src/Http.c | 40 +---- src/Menus.c | 29 +--- src/Platform.c | 39 ++--- src/Window.c | 258 ++++++++----------------------- src/interop_web.c | 379 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 464 insertions(+), 292 deletions(-) create mode 100644 src/interop_web.c diff --git a/src/Game.c b/src/Game.c index 41692110f..1b2c4c87b 100644 --- a/src/Game.c +++ b/src/Game.c @@ -510,16 +510,9 @@ void Game_TakeScreenshot(void) { String_Format3(&filename, "-%p2-%p2-%p2.png", &now.hour, &now.minute, &now.second); #ifdef CC_BUILD_WEB + extern void interop_TakeScreenshot(const char* path); Platform_EncodeUtf8(str, &filename); - EM_ASM_({ - var name = UTF8ToString($0); - var canvas = Module['canvas']; - if (canvas.toBlob) { - canvas.toBlob(function(blob) { Module.saveBlob(blob, name); }); - } else if (canvas.msToBlob) { - Module.saveBlob(canvas.msToBlob(), name); - } - }, str); + interop_TakeScreenshot(str); #elif CC_BUILD_MINFILES /* no screenshots for these systems */ #else diff --git a/src/Http.c b/src/Http.c index fc5974aba..9183b5e3c 100644 --- a/src/Http.c +++ b/src/Http.c @@ -314,6 +314,8 @@ static void Http_SetRequestHeaders(struct HttpRequest* req) { #ifdef CC_BUILD_WEB #include #include "Errors.h" +extern void interop_DownloadAsync(const char* url, int method); +extern int interop_IsHttpsOnly(void); cc_bool Http_DescribeError(cc_result res, cc_string* dst) { return false; } /* web browsers do caching already, so don't need last modified/etags */ @@ -346,46 +348,12 @@ static void Http_DownloadAsync(struct HttpRequest* req) { String_InitArray(url, urlBuffer); Http_BeginRequest(req, &url); Platform_EncodeUtf8(urlStr, &url); - - /* onFinished = FUNC(data, len, status) */ - /* onProgress = FUNC(read, total) */ - EM_ASM_({ - var url = UTF8ToString($0); - var reqMethod = $1 == 1 ? 'HEAD' : 'GET'; - var onFinished = Module["_Http_OnFinishedAsync"]; - var onProgress = Module["_Http_OnUpdateProgress"]; - - var xhr = new XMLHttpRequest(); - xhr.open(reqMethod, url); - xhr.responseType = 'arraybuffer'; - - var getContentLength = function(e) { - if (e.total) return e.total; - - try { - var len = xhr.getResponseHeader('Content-Length'); - return parseInt(len, 10); - } catch (ex) { return 0; } - }; - - xhr.onload = function(e) { - var src = new Uint8Array(xhr.response); - var len = src.byteLength; - var data = _malloc(len); - HEAPU8.set(src, data); - onFinished(data, len || getContentLength(e), xhr.status); - }; - xhr.onerror = function(e) { onFinished(0, 0, xhr.status); }; - xhr.ontimeout = function(e) { onFinished(0, 0, xhr.status); }; - xhr.onprogress = function(e) { onProgress(e.loaded, e.total); }; - - try { xhr.send(); } catch (e) { onFinished(0, 0, 0); } - }, urlStr, req->requestType); + interop_DownloadAsync(urlStr, req->requestType); } static void Http_WorkerInit(void) { /* If this webpage is https://, browsers deny any http:// downloading */ - httpsOnly = EM_ASM_INT_V({ return location.protocol === 'https:'; }); + httpsOnly = interop_IsHttpsOnly(); } static void Http_WorkerStart(void) { } static void Http_WorkerStop(void) { } diff --git a/src/Menus.c b/src/Menus.c index 342053dff..606b8f873 100644 --- a/src/Menus.c +++ b/src/Menus.c @@ -1320,7 +1320,7 @@ static void SaveLevelScreen_RemoveOverwrites(struct SaveLevelScreen* s) { } #ifdef CC_BUILD_WEB -#include +extern int interop_DownloadMap(const char* path, const char* filename); static void DownloadMap(const cc_string* path) { char strPath[NATIVE_STR_LEN]; char strFile[NATIVE_STR_LEN]; @@ -1333,21 +1333,8 @@ static void DownloadMap(const cc_string* path) { file.length = String_LastIndexOf(&file, '.'); String_AppendConst(&file, ".cw"); Platform_EncodeUtf8(strFile, &file); - - res = EM_ASM_({ - try { - var name = UTF8ToString($0); - var data = FS.readFile(name); - var blob = new Blob([data], { type: 'application/octet-stream' }); - Module.saveBlob(blob, UTF8ToString($1)); - FS.unlink(name); - return 0; - } catch (e) { - if (!(e instanceof FS.ErrnoError)) abort(e); - return -e.errno; - } - }, strPath, strFile); - + + res = interop_DownloadMap(strPath, strFile); if (res) { Logger_SysWarn2(res, "Downloading map", &file); } else { @@ -1594,18 +1581,12 @@ static void TexturePackScreen_LoadEntries(struct ListScreen* s) { } #ifdef CC_BUILD_WEB -#include +extern void interop_UploadTexPack(const char* path); static void TexturePackScreen_UploadCallback(const cc_string* path) { char str[NATIVE_STR_LEN]; Platform_EncodeUtf8(str, path); - /* Move from temp into texpacks folder */ - /* TODO: This is pretty awful and should be rewritten */ - EM_ASM_({ - var name = UTF8ToString($0);; - var data = FS.readFile(name); - FS.writeFile('/texpacks/' + name.substring(1), data); - }, str); + interop_UploadTexPack(str); TexturePackScreen_Show(); TexturePack_SetDefault(path); TexturePack_ExtractCurrent(true); diff --git a/src/Platform.c b/src/Platform.c index 213b0956f..937341a54 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -566,9 +566,10 @@ cc_result File_Close(cc_file file) { #ifndef CC_BUILD_WEB return close(file) == -1 ? errno : 0; #else - int ret = close(file) == -1 ? errno : 0; - EM_ASM( FS.syncfs(false, function(err) { if (err) console.log(err); }); ); - return ret; + extern void interop_SyncFS(void); + int res = close(file) == -1 ? errno : 0; + interop_SyncFS(); + return res; #endif } @@ -1098,10 +1099,11 @@ cc_result Process_StartOpen(const cc_string* args) { cc_result Process_StartGame(const cc_string* args) { return ERR_NOT_SUPPORTED; } void Process_Exit(cc_result code) { exit(code); } +extern void interop_OpenTab(const char* url); cc_result Process_StartOpen(const cc_string* args) { char str[NATIVE_STR_LEN]; Platform_EncodeUtf8(str, args); - EM_ASM_({ window.open(UTF8ToString($0)); }, str); + interop_OpenTab(str); return 0; } #elif defined CC_BUILD_ANDROID @@ -1730,35 +1732,18 @@ void Platform_Init(void) { Platform_InitSpecific(); } #elif defined CC_BUILD_WEB +extern void interop_InitModule(void); +extern void interop_GetIndexedDBError(char* buffer); void Platform_Init(void) { char tmp[64+1] = { 0 }; - EM_ASM( Module['websocket']['subprotocol'] = 'ClassiCube'; ); + interop_InitModule(); + /* Check if an error occurred when pre-loading IndexedDB */ - EM_ASM_({ if (window.cc_idbErr) stringToUTF8(window.cc_idbErr, $0, 64); }, tmp); - - EM_ASM({ - Module.saveBlob = function(blob, name) { - if (window.navigator.msSaveBlob) { - window.navigator.msSaveBlob(blob, name); return; - } - var url = window.URL.createObjectURL(blob); - var elem = document.createElement('a'); - - elem.href = url; - elem.download = name; - elem.style.display = 'none'; - - document.body.appendChild(elem); - elem.click(); - document.body.removeChild(elem); - window.URL.revokeObjectURL(url); - } - }); - + interop_GetIndexedDBError(tmp); if (!tmp[0]) return; + Chat_Add1("&cError preloading IndexedDB: %c", tmp); Chat_AddRaw("&cPreviously saved settings/maps will be lost"); - /* NOTE: You must pre-load IndexedDB before main() */ /* (because pre-loading only works asynchronously) */ /* If you don't, you'll get errors later trying to sync local to remote */ diff --git a/src/Window.c b/src/Window.c index 686632a33..37f26967a 100644 --- a/src/Window.c +++ b/src/Window.c @@ -2746,17 +2746,19 @@ void Window_FreeFramebuffer(struct Bitmap* bmp) { #include #include #include "Game.h" -static cc_bool keyboardOpen, needResize; +extern int interop_CanvasWidth(void); +extern int interop_CanvasHeight(void); +extern int interop_ScreenWidth(void); +extern int interop_ScreenHeight(void); +static cc_bool keyboardOpen, needResize; static int RawDpiScale(int x) { return (int)(x * emscripten_get_device_pixel_ratio()); } -static int GetCanvasWidth(void) { return EM_ASM_INT_V({ return Module['canvas'].width }); } -static int GetCanvasHeight(void) { return EM_ASM_INT_V({ return Module['canvas'].height }); } -static int GetScreenWidth(void) { return RawDpiScale(EM_ASM_INT_V({ return screen.width; })); } -static int GetScreenHeight(void) { return RawDpiScale(EM_ASM_INT_V({ return screen.height; })); } +static int GetScreenWidth(void) { return RawDpiScale(interop_ScreenWidth()); } +static int GetScreenHeight(void) { return RawDpiScale(interop_ScreenHeight()); } static void UpdateWindowBounds(void) { - int width = GetCanvasWidth(); - int height = GetCanvasHeight(); + int width = interop_CanvasWidth(); + int height = interop_CanvasHeight(); if (width == WindowInfo.Width && height == WindowInfo.Height) return; WindowInfo.Width = width; @@ -2830,15 +2832,8 @@ static EM_BOOL OnMouseMove(int type, const EmscriptenMouseEvent* ev, void* data) return true; } -/* TODO: Also query mouse coordinates globally and reuse adjustXY here */ -/* Adjust from document coordinates to element coordinates */ -static void AdjustXY(int* x, int* y) { - EM_ASM_({ - var canvasRect = Module['canvas'].getBoundingClientRect(); - HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - canvasRect.left; - HEAP32[$1 >> 2] = HEAP32[$1 >> 2] - canvasRect.top; - }, x, y); -} +/* TODO: Also query mouse coordinates globally (in OnMouseMove) and reuse interop_AdjustXY here */ +extern void interop_AdjustXY(int* x, int* y); static EM_BOOL OnTouchStart(int type, const EmscriptenTouchEvent* ev, void* data) { const EmscriptenTouchPoint* t; @@ -2848,7 +2843,7 @@ static EM_BOOL OnTouchStart(int type, const EmscriptenTouchEvent* ev, void* data if (!t->isChanged) continue; x = t->targetX; y = t->targetY; - AdjustXY( &x, &y); + interop_AdjustXY(&x, &y); RescaleXY(&x, &y); Input_AddTouch(t->identifier, x, y); } @@ -2865,7 +2860,7 @@ static EM_BOOL OnTouchMove(int type, const EmscriptenTouchEvent* ev, void* data) if (!t->isChanged) continue; x = t->targetX; y = t->targetY; - AdjustXY( &x, &y); + interop_AdjustXY(&x, &y); RescaleXY(&x, &y); Input_UpdateTouch(t->identifier, x, y); } @@ -2882,7 +2877,7 @@ static EM_BOOL OnTouchEnd(int type, const EmscriptenTouchEvent* ev, void* data) if (!t->isChanged) continue; x = t->targetX; y = t->targetY; - AdjustXY( &x, &y); + interop_AdjustXY(&x, &y); RescaleXY(&x, &y); Input_RemoveTouch(t->identifier, x, y); } @@ -3087,6 +3082,10 @@ static void UnhookEvents(void) { emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, 0, NULL); } +extern int interop_IsAndroid(void); +extern int interop_IsIOS(void); +extern void interop_InitClipboardListeners(void); +extern void interop_ForceTouchPageLayout(void); void Window_Init(void) { int is_ios, droid; DisplayInfo.Width = GetScreenWidth(); @@ -3095,40 +3094,10 @@ void Window_Init(void) { DisplayInfo.ScaleX = emscripten_get_device_pixel_ratio(); DisplayInfo.ScaleY = DisplayInfo.ScaleX; + interop_AddClipboardListeners(); - /* Copy text, but only if user isn't selecting something else on the webpage */ - /* (don't check window.clipboardData here, that's handled in Clipboard_SetText instead) */ - EM_ASM(window.addEventListener('copy', - function(e) { - if (window.getSelection && window.getSelection().toString()) return; - ccall('Window_RequestClipboardText', 'void'); - if (!window.cc_copyText) return; - - if (e.clipboardData) { - e.clipboardData.setData('text/plain', window.cc_copyText); - e.preventDefault(); - } - window.cc_copyText = null; - }); - ); - - /* Paste text (window.clipboardData is handled in Clipboard_GetText instead) */ - EM_ASM(window.addEventListener('paste', - function(e) { - if (e.clipboardData) { - var contents = e.clipboardData.getData('text/plain'); - ccall('Window_GotClipboardText', 'void', ['string'], [contents]); - } - }); - ); - - droid = EM_ASM_INT_V({ return /Android/i.test(navigator.userAgent); }); - /* iOS 13 on iPad doesn't identify itself as iPad by default anymore */ - /* https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up */ - is_ios = EM_ASM_INT_V({ - return /iPhone|iPad|iPod/i.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints && navigator.maxTouchPoints > 2); - }); + droid = interop_IsAndroid(); + is_ios = interop_IsIOS(); Input_SetTouchMode(is_ios || droid); /* iOS shifts the whole webpage up when opening chat, which causes problems */ @@ -3139,38 +3108,25 @@ void Window_Init(void) { /* Let the webpage know it needs to force a mobile layout */ if (!Input_TouchMode) return; - EM_ASM( if (typeof(forceTouchLayout) === 'function') forceTouchLayout(); ); + interop_ForceTouchPageLayout(); } +extern void interop_InitContainer(void); void Window_Create(int width, int height) { WindowInfo.Exists = true; WindowInfo.Focused = true; HookEvents(); /* Let the webpage decide on initial bounds */ - WindowInfo.Width = GetCanvasWidth(); - WindowInfo.Height = GetCanvasHeight(); - - /* Create wrapper div if necessary */ - EM_ASM({ - var agent = navigator.userAgent; - var canvas = Module['canvas']; - window.cc_container = document.body; - - if (/Android/i.test(agent) && /Chrome/i.test(agent)) { - var wrapper = document.createElement("div"); - wrapper.id = 'canvas_wrapper'; - - canvas.parentNode.insertBefore(wrapper, canvas); - wrapper.appendChild(canvas); - window.cc_container = wrapper; - } - }); + WindowInfo.Width = interop_CanvasWidth(); + WindowInfo.Height = interop_CanvasHeight(); + interop_InitContainer(); } +extern void interop_SetPageTitle(const char* title); void Window_SetTitle(const cc_string* title) { char str[NATIVE_STR_LEN]; Platform_EncodeUtf8(str, title); - EM_ASM_({ document.title = UTF8ToString($0); }, str); + interop_SetPageTitle(str); } static char pasteBuffer[512]; @@ -3189,31 +3145,20 @@ EMSCRIPTEN_KEEPALIVE void Window_GotClipboardText(char* src) { Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_PASTE, 0); } +extern void interop_TryGetClipboardText(void); void Clipboard_GetText(cc_string* value) { - /* For IE11, use window.clipboardData to get the clipboard */ - EM_ASM_({ - if (window.clipboardData) { - var contents = window.clipboardData.getData('Text'); - ccall('Window_StoreClipboardText', 'void', ['string'], [contents]); - } - }); + /* Window_StoreClipboardText may or may not be called by this */ + interop_TryGetClipboardText(); + String_Copy(value, &pasteStr); pasteStr.length = 0; } + +extern void interop_TrySetClipboardText(const char* text); void Clipboard_SetText(const cc_string* value) { char str[NATIVE_STR_LEN]; Platform_EncodeUtf8(str, value); - - /* For IE11, use window.clipboardData to set the clipboard */ - /* For other browsers, instead use the window.copy events */ - EM_ASM_({ - if (window.clipboardData) { - if (window.getSelection && window.getSelection().toString()) return; - window.clipboardData.setData('Text', UTF8ToString($0)); - } else { - window.cc_copyText = UTF8ToString($0); - } - }, str); + interop_TrySetClipboardText(str); } void Window_Show(void) { } @@ -3224,6 +3169,8 @@ int Window_GetWindowState(void) { return status.isFullscreen ? WINDOW_STATE_FULLSCREEN : WINDOW_STATE_NORMAL; } +extern int interop_GetContainerID(void); +extern void interop_EnterFullscreen(void); cc_result Window_EnterFullscreen(void) { EmscriptenFullscreenStrategy strategy; const char* target; @@ -3235,28 +3182,15 @@ cc_result Window_EnterFullscreen(void) { strategy.canvasResizedCallback = OnCanvasResize; strategy.canvasResizedCallbackUserData = NULL; - /* For chrome on android, need to make container div fullscreen instead */ - res = EM_ASM_INT_V({ return document.getElementById('canvas_wrapper') ? 1 : 0; }); + /* TODO: Return container element ID instead of hardcoding here */ + res = interop_GetContainerID(); target = res ? "canvas_wrapper" : "#canvas"; res = emscripten_request_fullscreen_strategy(target, 1, &strategy); if (res == EMSCRIPTEN_RESULT_NOT_SUPPORTED) res = ERR_NOT_SUPPORTED; if (res) return res; - /* emscripten sets css size to screen's base width/height, */ - /* except that becomes wrong when device rotates. */ - /* Better to just set CSS width/height to always be 100% */ - EM_ASM({ - var canvas = Module['canvas']; - canvas.style.width = '100%'; - canvas.style.height = '100%'; - }); - - /* By default, pressing Escape will immediately exit fullscreen - which is */ - /* quite annoying given that it is also the Menu key. Some browsers allow */ - /* 'locking' the Escape key, so that you have to hold down Escape to exit. */ - /* NOTE: This ONLY works when the webpage is a https:// one */ - EM_ASM({ try { navigator.keyboard.lock(["Escape"]); } catch (ex) { } }); + interop_EnterFullscreen(); return 0; } @@ -3286,6 +3220,7 @@ void Window_Close(void) { UnhookEvents(); } +extern void interop_RequestCanvasResize(void); void Window_ProcessEvents(void) { if (!needResize) return; needResize = false; @@ -3294,7 +3229,8 @@ void Window_ProcessEvents(void) { if (Window_GetWindowState() == WINDOW_STATE_FULLSCREEN) { SetFullscreenBounds(); } else { - EM_ASM( if (typeof(resizeGameCanvas) === 'function') resizeGameCanvas(); ); + /* Webpage can adjust canvas size if it wants to */ + interop_RequestCanvasResize(); } UpdateWindowBounds(); } @@ -3304,16 +3240,14 @@ static void Cursor_GetRawPos(int* x, int* y) { *x = 0; *y = 0; } /* Not allowed to move cursor from javascript */ void Cursor_SetPosition(int x, int y) { } +extern void interop_SetCursorVisible(int visible); static void Cursor_DoSetVisible(cc_bool visible) { - if (visible) { - EM_ASM(Module['canvas'].style['cursor'] = 'default'; ); - } else { - EM_ASM(Module['canvas'].style['cursor'] = 'none'; ); - } + interop_SetCursorVisible(visible); } -static void ShowDialogCore(const char* title, const char* msg) { - EM_ASM_({ alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); }, title, msg); +extern void interop_ShowDialog(const char* title, const char* msg); +static void ShowDialogCore(const char* title, const char* msg) { + interop_ShowDialog(title, msg); } static OpenFileDialogCallback uploadCallback; @@ -3326,39 +3260,11 @@ EMSCRIPTEN_KEEPALIVE void Window_OnFileUploaded(const char* src) { uploadCallback = NULL; } +extern void interop_OpenFileDialog(const char* filter); cc_result Window_OpenFileDialog(const char* filter, OpenFileDialogCallback callback) { uploadCallback = callback; - EM_ASM_({ - var elem = window.cc_uploadElem; - if (!elem) { - elem = document.createElement('input'); - elem.setAttribute('type', 'file'); - elem.setAttribute('style', 'display: none'); - elem.accept = UTF8ToString($0); - - elem.addEventListener('change', - function(ev) { - var files = ev.target.files; - for (var i = 0; i < files.length; i++) { - var reader = new FileReader(); - var name = files[i].name; - - reader.onload = function(e) { - var data = new Uint8Array(e.target.result); - FS.createDataFile('/', name, data, true, true, true); - ccall('Window_OnFileUploaded', 'void', ['string'], ['/' + name]); - FS.unlink('/' + name); - }; - reader.readAsArrayBuffer(files[i]); - } - window.cc_container.removeChild(window.cc_uploadElem); - window.cc_uploadElem = null; - }, false); - window.cc_uploadElem = elem; - window.cc_container.appendChild(elem); - } - elem.click(); - }, filter); + /* Calls Window_OnFileUploaded on success */ + interop_OpenFileDialog(filter); return ERR_NOT_SUPPORTED; } @@ -3366,6 +3272,10 @@ void Window_AllocFramebuffer(struct Bitmap* bmp) { } void Window_DrawFramebuffer(Rect2D r) { } void Window_FreeFramebuffer(struct Bitmap* bmp) { } +extern void interop_OpenKeyboard(const char* text, int type, const char* placeholder); +extern void interop_SetKeyboardText(const char* text); +extern void interop_CloseKeyboard(void); + EMSCRIPTEN_KEEPALIVE void Window_OnTextChanged(const char* src) { cc_string str; char buffer[800]; String_InitArray(str, buffer); @@ -3378,64 +3288,24 @@ void Window_OpenKeyboard(const struct OpenKeyboardArgs* args) { char str[NATIVE_STR_LEN]; keyboardOpen = true; if (!Input_TouchMode) return; + Platform_EncodeUtf8(str, args->text); Platform_LogConst("OPEN SESAME"); - - EM_ASM_({ - var elem = window.cc_inputElem; - if (!elem) { - if ($1 == 1) { - elem = document.createElement('input'); - elem.setAttribute('inputmode', 'decimal'); - } else { - elem = document.createElement('textarea'); - } - elem.setAttribute('style', 'position:absolute; left:0; bottom:0; margin: 0px; width: 100%'); - elem.setAttribute('placeholder', UTF8ToString($2)); - elem.value = UTF8ToString($0); - - elem.addEventListener('input', - function(ev) { - ccall('Window_OnTextChanged', 'void', ['string'], [ev.target.value]); - }, false); - window.cc_inputElem = elem; - - window.cc_divElem = document.createElement('div'); - window.cc_divElem.setAttribute('style', 'position:absolute; left:0; top:0; width:100%; height:100%; background-color: black; opacity:0.4; resize:none; pointer-events:none;'); - - window.cc_container.appendChild(window.cc_divElem); - window.cc_container.appendChild(elem); - } - elem.focus(); - elem.click(); - }, str, args->type, args->placeholder); + interop_OpenKeyboard(str, args->type, args->placeholder); } void Window_SetKeyboardText(const cc_string* text) { char str[NATIVE_STR_LEN]; if (!Input_TouchMode) return; + Platform_EncodeUtf8(str, text); - - EM_ASM_({ - if (!window.cc_inputElem) return; - var str = UTF8ToString($0); - - if (str == window.cc_inputElem.value) return; - window.cc_inputElem.value = str; - }, str); + interop_SetKeyboardText(str); } void Window_CloseKeyboard(void) { keyboardOpen = false; if (!Input_TouchMode) return; - - EM_ASM({ - if (!window.cc_inputElem) return; - window.cc_container.removeChild(window.cc_divElem); - window.cc_container.removeChild(window.cc_inputElem); - window.cc_divElem = null; - window.cc_inputElem = null; - }); + interop_CloseKeyboard(); } void Window_EnableRawMouse(void) { @@ -4497,15 +4367,11 @@ void GLContext_SetFpsLimit(cc_bool vsync, float minFrameMs) { } } +extern void interop_GetGpuRenderer(char buffer, int len); void GLContext_GetApiInfo(cc_string* info) { char buffer[NATIVE_STR_LEN]; int len; - - EM_ASM_({ - var dbg = GLctx.getExtension('WEBGL_debug_renderer_info'); - var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ""; - stringToUTF8(str, $0, $1); - }, buffer, NATIVE_STR_LEN); + interop_GetGpuRenderer(buffer, NATIVE_STR_LEN); len = String_CalcLen(buffer, NATIVE_STR_LEN); if (!len) return; diff --git a/src/interop_web.c b/src/interop_web.c new file mode 100644 index 000000000..5e1de6854 --- /dev/null +++ b/src/interop_web.c @@ -0,0 +1,379 @@ +#include "Core.h" + +#ifdef CC_BUILD_WEB +#include + +void interop_InitModule(void) { + EM_ASM({ + Module['websocket']['subprotocol'] = 'ClassiCube'; + + Module.saveBlob = function(blob, name) { + if (window.navigator.msSaveBlob) { + window.navigator.msSaveBlob(blob, name); return; + } + var url = window.URL.createObjectURL(blob); + var elem = document.createElement('a'); + + elem.href = url; + elem.download = name; + elem.style.display = 'none'; + + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); + window.URL.revokeObjectURL(url); + } + }); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Game----------------------------------------------------------* +*#########################################################################################################################*/ +void interop_TakeScreenshot(const char* path) { + EM_ASM_({ + var name = UTF8ToString($0); + var canvas = Module['canvas']; + if (canvas.toBlob) { + canvas.toBlob(function(blob) { Module.saveBlob(blob, name); }); + } else if (canvas.msToBlob) { + Module.saveBlob(canvas.msToBlob(), name); + } + }, path); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Http----------------------------------------------------------* +*#########################################################################################################################*/ +void interop_DownloadAsync(const char* urlStr, int method) { + /* onFinished = FUNC(data, len, status) */ + /* onProgress = FUNC(read, total) */ + EM_ASM_({ + var url = UTF8ToString($0); + var reqMethod = $1 == 1 ? 'HEAD' : 'GET'; + var onFinished = Module["_Http_OnFinishedAsync"]; + var onProgress = Module["_Http_OnUpdateProgress"]; + + var xhr = new XMLHttpRequest(); + xhr.open(reqMethod, url); + xhr.responseType = 'arraybuffer'; + + var getContentLength = function(e) { + if (e.total) return e.total; + + try { + var len = xhr.getResponseHeader('Content-Length'); + return parseInt(len, 10); + } catch (ex) { return 0; } + }; + + xhr.onload = function(e) { + var src = new Uint8Array(xhr.response); + var len = src.byteLength; + var data = _malloc(len); + HEAPU8.set(src, data); + onFinished(data, len || getContentLength(e), xhr.status); + }; + xhr.onerror = function(e) { onFinished(0, 0, xhr.status); }; + xhr.ontimeout = function(e) { onFinished(0, 0, xhr.status); }; + xhr.onprogress = function(e) { onProgress(e.loaded, e.total); }; + + try { xhr.send(); } catch (e) { onFinished(0, 0, 0); } + }, urlStr, method); +} + +int interop_IsHttpsOnly(void) { + /* If this webpage is https://, browsers deny any http:// downloading */ + return EM_ASM_INT_V({ return location.protocol === 'https:'; }); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Menu----------------------------------------------------------* +*#########################################################################################################################*/ +int interop_DownloadMap(const char* path, const char* filename) { + return EM_ASM_({ + try { + var name = UTF8ToString($0); + var data = FS.readFile(name); + var blob = new Blob([data], { type: 'application/octet-stream' }); + Module.saveBlob(blob, UTF8ToString($1)); + FS.unlink(name); + return 0; + } catch (e) { + if (!(e instanceof FS.ErrnoError)) abort(e); + return -e.errno; + } + }, path, filename); +} + +void interop_UploadTexPack(const char* path) { + /* Move from temp into texpacks folder */ + /* TODO: This is pretty awful and should be rewritten */ + EM_ASM_({ + var name = UTF8ToString($0); + var data = FS.readFile(name); + FS.writeFile('/texpacks/' + name.substring(1), data); + }, path); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Platform--------------------------------------------------------* +*#########################################################################################################################*/ +void interop_GetIndexedDBError(char* buffer) { + EM_ASM_({ if (window.cc_idbErr) stringToUTF8(window.cc_idbErr, $0, 64); }, buffer); +} + +void interop_SyncFS(void) { + EM_ASM( FS.syncfs(false, function(err) { if (err) console.log(err); }); ); +} + +void interop_OpenTab(const char* url) { + EM_ASM_({ window.open(UTF8ToString($0)); }, url); + return 0; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Window---------------------------------------------------------* +*#########################################################################################################################*/ +int interop_CanvasWidth(void) { return EM_ASM_INT_V({ return Module['canvas'].width }); } +int interop_CanvasHeight(void) { return EM_ASM_INT_V({ return Module['canvas'].height }); } +int interop_ScreenWidth(void) { return EM_ASM_INT_V({ return screen.width; }); } +int interop_ScreenHeight(void) { return EM_ASM_INT_V({ return screen.height; }); } + +int interop_IsAndroid(void) { + return EM_ASM_INT_V({ return /Android/i.test(navigator.userAgent); }); +} +int interop_IsIOS(void) { + /* iOS 13 on iPad doesn't identify itself as iPad by default anymore */ + /* https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up */ + return EM_ASM_INT_V({ + return /iPhone|iPad|iPod/i.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints && navigator.maxTouchPoints > 2); + }); +} + +void interop_InitContainer(void) { + /* Create wrapper div if necessary */ + EM_ASM({ + var agent = navigator.userAgent; + var canvas = Module['canvas']; + window.cc_container = document.body; + + if (/Android/i.test(agent) && /Chrome/i.test(agent)) { + var wrapper = document.createElement("div"); + wrapper.id = 'canvas_wrapper'; + + canvas.parentNode.insertBefore(wrapper, canvas); + wrapper.appendChild(canvas); + window.cc_container = wrapper; + } + }); +} + +int interop_GetContainerID(void) { + /* For chrome on android, need to make container div fullscreen instead */ + return EM_ASM_INT_V({ return document.getElementById('canvas_wrapper') ? 1 : 0; }); +} + +void interop_ForceTouchPageLayout(void) { + EM_ASM( if (typeof(forceTouchLayout) === 'function') forceTouchLayout(); ); +} + +void interop_SetPageTitle(const char* title) { + EM_ASM_({ document.title = UTF8ToString($0); }, title); +} + +void interop_AddClipboardListeners(void) { + /* Copy text, but only if user isn't selecting something else on the webpage */ + /* (don't check window.clipboardData here, that's handled in interop_TrySetClipboardText instead) */ + EM_ASM(window.addEventListener('copy', + function(e) { + if (window.getSelection && window.getSelection().toString()) return; + ccall('Window_RequestClipboardText', 'void'); + if (!window.cc_copyText) return; + + if (e.clipboardData) { + e.clipboardData.setData('text/plain', window.cc_copyText); + e.preventDefault(); + } + window.cc_copyText = null; + }); + ); + + /* Paste text (window.clipboardData is handled in interop_TryGetClipboardText instead) */ + EM_ASM(window.addEventListener('paste', + function(e) { + if (e.clipboardData) { + var contents = e.clipboardData.getData('text/plain'); + ccall('Window_GotClipboardText', 'void', ['string'], [contents]); + } + }); + ); +} + +void interop_TryGetClipboardText(void) { + /* For IE11, use window.clipboardData to get the clipboard */ + EM_ASM_({ + if (window.clipboardData) { + var contents = window.clipboardData.getData('Text'); + ccall('Window_StoreClipboardText', 'void', ['string'], [contents]); + } + }); +} + +void interop_TrySetClipboardText(const char* text) { + /* For IE11, use window.clipboardData to set the clipboard */ + /* For other browsers, instead use the window.copy events */ + EM_ASM_({ + if (window.clipboardData) { + if (window.getSelection && window.getSelection().toString()) return; + window.clipboardData.setData('Text', UTF8ToString($0)); + } else { + window.cc_copyText = UTF8ToString($0); + } + }, text); +} + +void interop_EnterFullscreen(void) { + /* emscripten sets css size to screen's base width/height, */ + /* except that becomes wrong when device rotates. */ + /* Better to just set CSS width/height to always be 100% */ + EM_ASM({ + var canvas = Module['canvas']; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + }); + + /* By default, pressing Escape will immediately exit fullscreen - which is */ + /* quite annoying given that it is also the Menu key. Some browsers allow */ + /* 'locking' the Escape key, so that you have to hold down Escape to exit. */ + /* NOTE: This ONLY works when the webpage is a https:// one */ + EM_ASM({ try { navigator.keyboard.lock(["Escape"]); } catch (ex) { } }); +} + +/* Adjust from document coordinates to element coordinates */ +void interop_AdjustXY(int* x, int* y) { + EM_ASM_({ + var canvasRect = Module['canvas'].getBoundingClientRect(); + HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - canvasRect.left; + HEAP32[$1 >> 2] = HEAP32[$1 >> 2] - canvasRect.top; + }, x, y); +} + +void interop_RequestCanvasResize(void) { + EM_ASM( if (typeof(resizeGameCanvas) === 'function') resizeGameCanvas(); ); +} + +void interop_SetCursorVisible(int visible) { + if (visible) { + EM_ASM(Module['canvas'].style['cursor'] = 'default'; ); + } else { + EM_ASM(Module['canvas'].style['cursor'] = 'none'; ); + } +} + +void interop_ShowDialog(const char* title, const char* msg) { + EM_ASM_({ alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); }, title, msg); +} + +void interop_OpenKeyboard(const char* text, int type, const char* placeholder) { + EM_ASM_({ + var elem = window.cc_inputElem; + if (!elem) { + if ($1 == 1) { + elem = document.createElement('input'); + elem.setAttribute('inputmode', 'decimal'); + } else { + elem = document.createElement('textarea'); + } + elem.setAttribute('style', 'position:absolute; left:0; bottom:0; margin: 0px; width: 100%'); + elem.setAttribute('placeholder', UTF8ToString($2)); + elem.value = UTF8ToString($0); + + elem.addEventListener('input', + function(ev) { + ccall('Window_OnTextChanged', 'void', ['string'], [ev.target.value]); + }, false); + window.cc_inputElem = elem; + + window.cc_divElem = document.createElement('div'); + window.cc_divElem.setAttribute('style', 'position:absolute; left:0; top:0; width:100%; height:100%; background-color: black; opacity:0.4; resize:none; pointer-events:none;'); + + window.cc_container.appendChild(window.cc_divElem); + window.cc_container.appendChild(elem); + } + elem.focus(); + elem.click(); + }, text, type, placeholder); +} + +void interop_SetKeyboardText(const char* text) { + EM_ASM_({ + if (!window.cc_inputElem) return; + var str = UTF8ToString($0); + + if (str == window.cc_inputElem.value) return; + window.cc_inputElem.value = str; + }, text); +} + +void interop_CloseKeyboard(void) { + EM_ASM({ + if (!window.cc_inputElem) return; + window.cc_container.removeChild(window.cc_divElem); + window.cc_container.removeChild(window.cc_inputElem); + window.cc_divElem = null; + window.cc_inputElem = null; + }); +} + +void interop_OpenFileDialog(const char* filter) { + EM_ASM_({ + var elem = window.cc_uploadElem; + if (!elem) { + elem = document.createElement('input'); + elem.setAttribute('type', 'file'); + elem.setAttribute('style', 'display: none'); + elem.accept = UTF8ToString($0); + + elem.addEventListener('change', + function(ev) { + var files = ev.target.files; + for (var i = 0; i < files.length; i++) { + var reader = new FileReader(); + var name = files[i].name; + + reader.onload = function(e) { + var data = new Uint8Array(e.target.result); + FS.createDataFile('/', name, data, true, true, true); + ccall('Window_OnFileUploaded', 'void', ['string'], ['/' + name]); + FS.unlink('/' + name); + }; + reader.readAsArrayBuffer(files[i]); + } + window.cc_container.removeChild(window.cc_uploadElem); + window.cc_uploadElem = null; + }, false); + window.cc_uploadElem = elem; + window.cc_container.appendChild(elem); + } + elem.click(); + }, filter); +} + + +/*########################################################################################################################* +*--------------------------------------------------------GLContext--------------------------------------------------------* +*#########################################################################################################################*/ +void interop_GetGpuRenderer(char buffer, int len) { + EM_ASM_({ + var dbg = GLctx.getExtension('WEBGL_debug_renderer_info'); + var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ""; + stringToUTF8(str, $0, $1); + }, buffer, len); +} +#endif From e79538d3a62afd489df1c42c18b2db06643f5805 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Fri, 30 Apr 2021 19:34:42 +1000 Subject: [PATCH 2/3] Fix multiple issues with moving to separate file --- src/Platform.c | 5 ++--- src/Window.c | 2 +- src/interop_web.c | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Platform.c b/src/Platform.c index 937341a54..3814788ed 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -1099,12 +1099,11 @@ cc_result Process_StartOpen(const cc_string* args) { cc_result Process_StartGame(const cc_string* args) { return ERR_NOT_SUPPORTED; } void Process_Exit(cc_result code) { exit(code); } -extern void interop_OpenTab(const char* url); +extern int interop_OpenTab(const char* url); cc_result Process_StartOpen(const cc_string* args) { char str[NATIVE_STR_LEN]; Platform_EncodeUtf8(str, args); - interop_OpenTab(str); - return 0; + return interop_OpenTab(str); } #elif defined CC_BUILD_ANDROID static char gameArgsBuffer[512]; diff --git a/src/Window.c b/src/Window.c index 37f26967a..d78f6c261 100644 --- a/src/Window.c +++ b/src/Window.c @@ -3084,7 +3084,7 @@ static void UnhookEvents(void) { extern int interop_IsAndroid(void); extern int interop_IsIOS(void); -extern void interop_InitClipboardListeners(void); +extern void interop_AddClipboardListeners(void); extern void interop_ForceTouchPageLayout(void); void Window_Init(void) { int is_ios, droid; diff --git a/src/interop_web.c b/src/interop_web.c index 5e1de6854..580054032 100644 --- a/src/interop_web.c +++ b/src/interop_web.c @@ -130,7 +130,7 @@ void interop_SyncFS(void) { EM_ASM( FS.syncfs(false, function(err) { if (err) console.log(err); }); ); } -void interop_OpenTab(const char* url) { +int interop_OpenTab(const char* url) { EM_ASM_({ window.open(UTF8ToString($0)); }, url); return 0; } @@ -369,7 +369,7 @@ void interop_OpenFileDialog(const char* filter) { /*########################################################################################################################* *--------------------------------------------------------GLContext--------------------------------------------------------* *#########################################################################################################################*/ -void interop_GetGpuRenderer(char buffer, int len) { +void interop_GetGpuRenderer(char* buffer, int len) { EM_ASM_({ var dbg = GLctx.getExtension('WEBGL_debug_renderer_info'); var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ""; From be714984752b9314b412fc7c059a2b69396fb074 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Fri, 30 Apr 2021 20:51:32 +1000 Subject: [PATCH 3/3] fix 1 last issue, 5 browsers should be enough testing --- src/Window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Window.c b/src/Window.c index d78f6c261..392d0ba47 100644 --- a/src/Window.c +++ b/src/Window.c @@ -4367,7 +4367,7 @@ void GLContext_SetFpsLimit(cc_bool vsync, float minFrameMs) { } } -extern void interop_GetGpuRenderer(char buffer, int len); +extern void interop_GetGpuRenderer(char* buffer, int len); void GLContext_GetApiInfo(cc_string* info) { char buffer[NATIVE_STR_LEN]; int len;