Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9fe89b7
Feature - [WASM] Webview support
Namaneo Nov 5, 2024
1d99b4a
Merge tag '150-100-0' into wasm-webview
samuel-tranchet Nov 20, 2025
64fe12b
ignore vscode stuff here
samuel-tranchet Dec 5, 2025
676756f
wip -- stuff is working
mtrang1263 Dec 17, 2025
d72f7f8
cleanup several event listeners
mtrang1263 Dec 17, 2025
a04c48c
remove log statements
mtrang1263 Dec 17, 2025
cd8e44f
more cleanup
mtrang1263 Dec 17, 2025
15bcb78
final cleanup
mtrang1263 Dec 17, 2025
fe39763
Fix use of struct from WASM in JS for audio format
Kodikuu Feb 11, 2026
ee24069
Update flow to enable iframe via src instead of loading the entire do…
mtrang1263 Feb 20, 2026
1437d6d
fix focus event
mtrang1263 Feb 23, 2026
250870b
add new method to run loops on main thread
mtrang1263 Mar 4, 2026
f75c50d
Merge remote-tracking branch 'origin/stable' into wasm-webview
mtrang1263 Mar 4, 2026
4a43240
Fix use of struct from WASM in JS for audio format
Kodikuu Feb 11, 2026
c790b6a
add new method to run loops on main thread
mtrang1263 Mar 4, 2026
ade1e6d
cleanup
mtrang1263 Mar 4, 2026
9e0d664
Merge branch 'wasm-webview' into wasm-webview-iframe
mtrang1263 Mar 5, 2026
b3a403b
Merge branch 'mt-fix-webapp' into wasm-webview-iframe
mtrang1263 Mar 5, 2026
f1bb0d9
Merge branch 'mt-fix-webapp' into wasm-webview
mtrang1263 Mar 5, 2026
4d8c981
fix merge issue
mtrang1263 Mar 5, 2026
07a53b8
Merge branch 'wasm-webview-iframe' into wasm-webview
mtrang1263 Mar 5, 2026
4150fc1
remove hackytime fix
mtrang1263 Mar 5, 2026
d6f9430
fix exception suppression
mtrang1263 Mar 6, 2026
beb6b5d
Add new window method to set the web build
mtrang1263 Mar 9, 2026
33e4171
Fix opening of URLs on webview
mtrang1263 Mar 9, 2026
02d7fed
implement file deletion
mtrang1263 Mar 10, 2026
a8ce4d4
wip - willRead
mtrang1263 Mar 12, 2026
8ea5f10
wip - direct codex need to cleanup
mtrang1263 Mar 12, 2026
4322b59
Cleanup code
mtrang1263 Mar 12, 2026
29a7c82
Cleanup canvas
mtrang1263 Mar 12, 2026
01af3e8
More cleanup of JSPI state
mtrang1263 Mar 12, 2026
b266b9c
mo betta
mtrang1263 Mar 12, 2026
ac8b8eb
Merge branch 'mt-fix-webapp' into wasm-webview
mtrang1263 Mar 12, 2026
6fd3d07
Merge remote-tracking branch 'origin/stable' into wasm-webview
mtrang1263 Mar 19, 2026
f39f6c4
add title on iframe that render the webview
margauxbedu Jun 2, 2026
0e47136
Revert "add title on iframe that render the webview"
margauxbedu Jun 2, 2026
049a8cd
Add title on iframe
margauxbedu Jun 2, 2026
f05a071
add attributes to canvas
margauxbedu Jun 4, 2026
f90de59
Merge remote-tracking branch 'origin/stable' into wasm-webview
mtrang1263 Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.vscode/*

*.o
*.so
*.obj
Expand Down
42 changes: 41 additions & 1 deletion src/unix/web/matoya-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)});
},
};

Expand Down Expand Up @@ -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'});
},
};


Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
};

Expand Down
80 changes: 69 additions & 11 deletions src/unix/web/matoya.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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;
}
}
}
123 changes: 119 additions & 4 deletions src/unix/web/webview.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,98 @@

#include "webview.h"

#include <string.h>
#include <math.h>

#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)
Expand All @@ -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)
Expand All @@ -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);
}