diff --git a/.gitignore b/.gitignore index 53d76920..28468049 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode/* + *.o *.so *.obj diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 3c960025..2378bee0 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -728,7 +728,7 @@ const MTY_CRYPTO_API = { const MTY_SYSTEM_API = { MTY_HandleProtocol: function (uri, token) { - postMessage({type: 'uri', uri}); + postMessage({type: 'uri', uri: mty_str_to_js(uri)}); }, }; @@ -889,6 +889,31 @@ const MTY_WEB_API = { // NOTE: This will end up blocking the caller indefinitely. throw 'run_and_yield halted execution'; }, + web_webview_create: function(ctx) { + postMessage({type: 'wv-create', ctx}); + }, + web_webview_destroy: function() { + postMessage({type: 'wv-destroy'}); + }, + web_webview_navigate: function(csource, url) { + const source = mty_str_to_js(csource); + postMessage({type: 'wv-navigate', source, url}); + }, + web_webview_show: function(show) { + postMessage({type: 'wv-show', show}); + }, + web_webview_is_visible: function() { + postMessage({type: 'wv-is-visible', sync: MTY.sync, sab: MTY.sab}); + mty_wait(MTY.sync); + return MTY.sab[0] != 0; + }, + web_webview_send_text: function(cmessage) { + const message = mty_str_to_js(cmessage); + postMessage({type: 'wv-send-text', message}); + }, + web_webview_reload: function() { + postMessage({type: 'wv-reload'}); + }, }; @@ -1003,6 +1028,16 @@ const MTY_WASI_SNAPSHOT_PREVIEW1_API = { return __WASI_ERRNO_SUCCESS; }, path_unlink_file: function (fd, path) { + const jpath = mty_str_to_js(path); + + postMessage({ + type: 'remove-ls', + key: jpath, + sync: MTY.sync, + }); + + mty_wait(MTY.sync); + return __WASI_ERRNO_SUCCESS; }, path_readlink: function (fd, path, buf, buf_len, retptr0) { @@ -1319,6 +1354,11 @@ onmessage = async (ev) => { mty_free(cmem); break; } + case 'wv-event': + const buf = mty_alloc(1, msg.message.length + 1); + mty_str_to_c(msg.message, buf, msg.message.length + 1); + MTY.exports.mty_webview_handle_event(msg.ctx, buf); + break; } }; diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 4f9c677d..6a6056ec 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -324,17 +324,10 @@ function mty_add_input_events(thread) { ev.preventDefault(); }); - window.addEventListener('blur', (ev) => { + document.addEventListener('visibilitychange', (ev) => { thread.postMessage({ type: 'focus', - focus: false, - }); - }); - - window.addEventListener('focus', (ev) => { - thread.postMessage({ - type: 'focus', - focus: true, + focus: document.visibilityState == 'visible', }); }); @@ -848,6 +841,9 @@ async function MTY_Start(bin, container, userEnv) { MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); MTY.canvas.style.width = '100%'; MTY.canvas.style.height = '100%'; + MTY.canvas.setAttribute('role', 'img'); + MTY.canvas.setAttribute('aria-label', 'Parsec overlay and mouse cursor'); + container.appendChild(MTY.canvas); mty_update_canvas(MTY.canvas); @@ -951,6 +947,10 @@ async function mty_thread_message(ev) { window.localStorage[msg.key] = mty_buf_to_b64(msg.val); mty_signal(msg.sync); break; + case 'remove-ls': + window.localStorage.removeItem(msg.key); + mty_signal(msg.sync); + break; case 'alert': mty_alert(msg.title, msg.msg); break; @@ -997,7 +997,7 @@ async function mty_thread_message(ev) { break; case 'uri': mty_set_action(() => { - window.open(mty_str_to_js(msg.uri), '_blank'); + window.open(msg.uri, '_blank'); }); break; case 'http': { @@ -1082,5 +1082,63 @@ async function mty_thread_message(ev) { mty_signal(msg.sync); break; + case 'wv-create': + MTY.webview = document.createElement('iframe'); + MTY.webview.title = 'Parsec application'; + + MTY.webview.style.visibility = 'hidden'; + MTY.webview.style.position = 'fixed'; + MTY.webview.style.border = 'none'; + MTY.webview.style.width = '100%'; + MTY.webview.style.height = '100%'; + MTY.webview.style.inset = '0'; + + window.addEventListener('message', function (message) { + MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); + }); + + document.body.appendChild(MTY.webview); + break; + case 'wv-destroy': + document.body.removeChild(MTY.webview); + delete MTY.webview; + break; + case 'wv-navigate': + if (msg.url) { + MTY.webview.src = msg.source; + + MTY.webview.onload = () => { + try { + const script = MTY.webview.contentWindow.document.createElement('script'); + script.textContent = "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; + MTY.webview.contentWindow.document.head.appendChild(script); + const userAgentScript = MTY.webview.contentWindow.document.createElement('script'); + userAgentScript.textContent = "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; + MTY.webview.contentWindow.document.head.appendChild(userAgentScript); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } catch (e) { + console.error('Failed to inject script into iframe (cross-origin restriction):', e); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } + }; + } else { + const blob = new Blob([msg.source], { type: 'text/html' }); + MTY.webview.src = URL.createObjectURL(blob); + } + break; + case 'wv-show': + MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; + break; + case 'wv-is-visible': + msg.sab[0] = MTY.webview.style.visibility != 'visible'; + mty_signal(msg.sync); + break; + case 'wv-send-text': + // wv-send-text sends native app messages back to the running UI + MTY.webview.contentWindow.MTY_NativeListener(msg.message); + break; + case 'wv-reload': + MTY.webview.contentWindow.location.reload(); + break; } -} +} \ No newline at end of file diff --git a/src/unix/web/webview.c b/src/unix/web/webview.c index 34bde07a..8c20fbde 100644 --- a/src/unix/web/webview.c +++ b/src/unix/web/webview.c @@ -4,39 +4,98 @@ #include "webview.h" +#include +#include + +#include "matoya.h" +#include "web/keymap.h" + +struct webview { + MTY_App *app; + MTY_Window window; + WEBVIEW_READY ready_func; + WEBVIEW_TEXT text_func; + WEBVIEW_KEY key_func; + MTY_Hash *keys; + MTY_Queue *pushq; + bool ready; + bool passthrough; +}; + +void web_webview_create(struct webview *ctx); +void web_webview_destroy(); +void web_webview_navigate(const char *source, bool url); +void web_webview_show(bool show); +bool web_webview_is_visible(); +void web_webview_send_text(const char *message); +void web_webview_reload(); + struct webview *mty_webview_create(MTY_App *app, MTY_Window window, const char *dir, bool debug, WEBVIEW_READY ready_func, WEBVIEW_TEXT text_func, WEBVIEW_KEY key_func) { - return NULL; + struct webview *ctx = MTY_Alloc(1, sizeof(struct webview)); + + ctx->app = app; + ctx->window = window; + ctx->ready_func = ready_func; + ctx->text_func = text_func; + ctx->key_func = key_func; + + ctx->keys = web_keymap_hash(); + ctx->pushq = MTY_QueueCreate(50, 0); + + web_webview_create(ctx); + + return ctx; } void mty_webview_destroy(struct webview **webview) { + if (!webview || !*webview) + return; + + struct webview *ctx = *webview; + *webview = NULL; + + web_webview_destroy(); + + if (ctx->pushq) + MTY_QueueFlush(ctx->pushq, MTY_Free); + + MTY_QueueDestroy(&ctx->pushq); + MTY_HashDestroy(&ctx->keys, NULL); + + MTY_Free(ctx); } void mty_webview_navigate(struct webview *ctx, const char *source, bool url) { + web_webview_navigate(source, url); } void mty_webview_show(struct webview *ctx, bool show) { + web_webview_show(show); } bool mty_webview_is_visible(struct webview *ctx) { - return false; + return web_webview_is_visible(); } void mty_webview_send_text(struct webview *ctx, const char *msg) { + web_webview_send_text(msg); } void mty_webview_reload(struct webview *ctx) { + web_webview_reload(); } void mty_webview_set_input_passthrough(struct webview *ctx, bool passthrough) { + ctx->passthrough = passthrough; } bool mty_webview_event(struct webview *ctx, MTY_Event *evt) @@ -54,7 +113,7 @@ void mty_webview_render(struct webview *ctx) bool mty_webview_is_focussed(struct webview *ctx) { - return false; + return true; } bool mty_webview_is_steam(void) @@ -64,5 +123,61 @@ bool mty_webview_is_steam(void) bool mty_webview_is_available(void) { - return false; + return true; +} + +__attribute__((export_name("mty_webview_handle_event"))) +void mty_webview_handle_event(struct webview *ctx, char *str) +{ + MTY_JSON *j = NULL; + + switch (str[0]) { + // MTY_EVENT_WEBVIEW_READY + case 'R': + ctx->ready = true; + + // Send any queued messages before the WebView became ready + for (char *msg = NULL; MTY_QueuePopPtr(ctx->pushq, 0, (void **) &msg, NULL);) { + mty_webview_send_text(ctx, msg); + MTY_Free(msg); + } + + ctx->ready_func(ctx->app, ctx->window); + break; + + // MTY_EVENT_WEBVIEW_TEXT + case 'T': + ctx->text_func(ctx->app, ctx->window, str + 1); + break; + + // MTY_EVENT_KEY + case 'D': + case 'U': + if (!ctx->passthrough) + break; + + j = MTY_JSONParse(str + 1); + if (!j) + break; + + const char *code = MTY_JSONObjGetStringPtr(j, "code"); + if (!code) + break; + + uint32_t jmods = 0; + if (!MTY_JSONObjGetInt(j, "mods", (int32_t *) &jmods)) + break; + + MTY_Key key = (MTY_Key) (uintptr_t) MTY_HashGet(ctx->keys, code) & 0xFFFF; + if (key == MTY_KEY_NONE) + break; + + MTY_Mod mods = web_keymap_mods(jmods); + + ctx->key_func(ctx->app, ctx->window, str[0] == 'D', key, mods); + break; + } + + MTY_JSONDestroy(&j); + MTY_Free(str); }