Browse Source

DisplayServerJavaScript implementation.

Fabio Alessandrelli 5 years ago
parent
commit
7411e7fd37

+ 0 - 2
misc/dist/html/full-size.html

@@ -169,8 +169,6 @@ $GODOT_HEAD_INCLUDE
 				var height = window.innerHeight;
 				canvas.width = width * scale;
 				canvas.height = height * scale;
-				canvas.style.width = width + "px";
-				canvas.style.height = height + "px";
 			}
 			animationCallbacks.push(adjustCanvasDimensions);
 			adjustCanvasDimensions();

+ 15 - 4
platform/javascript/SCsub

@@ -4,6 +4,7 @@ Import("env")
 
 javascript_files = [
     "audio_driver_javascript.cpp",
+    "display_server_javascript.cpp",
     "http_client_javascript.cpp",
     "javascript_eval.cpp",
     "javascript_main.cpp",
@@ -46,11 +47,21 @@ wrap_list = [
 js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")
 
 zip_dir = env.Dir("#bin/.javascript_zip")
-out_files = [zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")]
-in_files = [js_wrapped, build[1], "#misc/dist/html/full-size.html"]
+binary_name = "godot.tools" if env["tools"] else "godot"
+out_files = [
+        zip_dir.File(binary_name + ".js"),
+        zip_dir.File(binary_name + ".wasm"),
+        zip_dir.File(binary_name + ".html")
+]
+html_file = "#misc/dist/html/full-size.html"
+in_files = [
+        js_wrapped,
+        build[1],
+        html_file
+]
 if env["threads_enabled"]:
     in_files.append(build[2])
-    out_files.append(zip_dir.File("godot.worker.js"))
+    out_files.append(zip_dir.File(binary_name + ".worker.js"))
 
 zip_files = env.InstallAs(out_files, in_files)
 env.Zip(
@@ -58,5 +69,5 @@ env.Zip(
     zip_files,
     ZIPROOT=zip_dir,
     ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}",
-    ZIPCOMSTR="Archving $SOURCES as $TARGET",
+    ZIPCOMSTR="Archving $SOURCES as $TARGET"
 )

+ 27 - 3
platform/javascript/audio_driver_javascript.cpp

@@ -190,19 +190,43 @@ void AudioDriverJavaScript::lock() {
 void AudioDriverJavaScript::unlock() {
 }
 
-void AudioDriverJavaScript::finish() {
+void AudioDriverJavaScript::finish_async() {
+
+	// Close the context, add the operation to the async_finish list in module.
+	int id = _driver_id;
+	_driver_id = 0;
 
 	/* clang-format off */
 	EM_ASM({
+		var ref = Module.IDHandler.get($0);
+		Module.async_finish.push(new Promise(function(accept, reject) {
+			if (!ref) {
+				console.log("Ref not found!", $0, Module.IDHandler);
+				setTimeout(accept, 0);
+			} else {
+				const context = ref['context'];
+				// Disconnect script and input.
+				ref['script'].disconnect();
+				if (ref['input'])
+					ref['input'].disconnect();
+				ref = null;
+				context.close().then(function() {
+					accept();
+				}).catch(function(e) {
+					accept();
+				});
+			}
+		}));
 		Module.IDHandler.remove($0);
-	}, _driver_id);
+	}, id);
 	/* clang-format on */
+}
 
+void AudioDriverJavaScript::finish() {
 	if (internal_buffer) {
 		memdelete_arr(internal_buffer);
 		internal_buffer = nullptr;
 	}
-	_driver_id = 0;
 }
 
 Error AudioDriverJavaScript::capture_start() {

+ 1 - 0
platform/javascript/audio_driver_javascript.h

@@ -56,6 +56,7 @@ public:
 	virtual void lock();
 	virtual void unlock();
 	virtual void finish();
+	void finish_async();
 
 	virtual Error capture_start();
 	virtual Error capture_stop();

+ 8 - 8
platform/javascript/detect.py

@@ -2,7 +2,6 @@ import os
 
 from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file
 
-
 def is_active():
     return True
 
@@ -17,12 +16,11 @@ def can_build():
 
 def get_opts():
     from SCons.Variables import BoolVariable
-
     return [
         # eval() can be a security concern, so it can be disabled.
         BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
         BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False),
-        BoolVariable("use_closure_compiler", "Use closure compiler to minimize Javascript code", False),
+        BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
     ]
 
 
@@ -57,7 +55,7 @@ def configure(env):
         env.Append(CPPDEFINES=["DEBUG_ENABLED"])
         # Retain function names for backtraces at the cost of file size.
         env.Append(LINKFLAGS=["--profiling-funcs"])
-    else:  # 'debug'
+    else: # "debug"
         env.Append(CPPDEFINES=["DEBUG_ENABLED"])
         env.Append(CCFLAGS=["-O1", "-g"])
         env.Append(LINKFLAGS=["-O1", "-g"])
@@ -150,7 +148,7 @@ def configure(env):
     env.Append(LIBS=["idbfs.js"])
 
     env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
-    env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", 'EXPORT_NAME="Godot"'])
+    env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"])
 
     # Allow increasing memory buffer size during runtime. This is efficient
     # when using WebAssembly (in comparison to asm.js) and works well for
@@ -162,8 +160,10 @@ def configure(env):
 
     env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
 
-    # callMain for manual start, FS for preloading.
-    env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain", "FS"]'])
+    # Allow use to take control of swapping WebGL buffers.
+    env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"])
 
+    # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS.
+    env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH', 'ERRNO_CODES']"])
     # Add code that allow exiting runtime.
-    env.Append(LINKFLAGS=['-s', 'EXIT_RUNTIME=1'])
+    env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"])

+ 1181 - 0
platform/javascript/display_server_javascript.cpp

@@ -0,0 +1,1181 @@
+#include "platform/javascript/display_server_javascript.h"
+
+#include "drivers/dummy/rasterizer_dummy.h"
+#include "platform/javascript/os_javascript.h"
+
+#include <emscripten.h>
+#include <png.h>
+
+#include "dom_keys.inc"
+
+#define DOM_BUTTON_LEFT 0
+#define DOM_BUTTON_MIDDLE 1
+#define DOM_BUTTON_RIGHT 2
+#define DOM_BUTTON_XBUTTON1 3
+#define DOM_BUTTON_XBUTTON2 4
+
+DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() {
+	return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton());
+}
+
+// Window (canvas)
+extern "C" EMSCRIPTEN_KEEPALIVE void _set_canvas_id(uint8_t *p_data, int p_data_size) {
+	DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton();
+	display->canvas_id.parse_utf8((const char *)p_data, p_data_size);
+	display->canvas_id = "#" + display->canvas_id;
+}
+
+static void focus_canvas() {
+
+	/* clang-format off */
+	EM_ASM(
+		Module['canvas'].focus();
+	);
+	/* clang-format on */
+}
+
+static bool is_canvas_focused() {
+
+	/* clang-format off */
+	return EM_ASM_INT_V(
+		return document.activeElement == Module['canvas'];
+	);
+	/* clang-format on */
+}
+
+static Point2 compute_position_in_canvas(int x, int y) {
+	DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton();
+	int canvas_x = EM_ASM_INT({
+		return Module['canvas'].getBoundingClientRect().x;
+	});
+	int canvas_y = EM_ASM_INT({
+		return Module['canvas'].getBoundingClientRect().y;
+	});
+	int canvas_width;
+	int canvas_height;
+	emscripten_get_canvas_element_size(display->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
+
+	double element_width;
+	double element_height;
+	emscripten_get_element_css_size(display->canvas_id.utf8().get_data(), &element_width, &element_height);
+
+	return Point2((int)(canvas_width / element_width * (x - canvas_x)),
+			(int)(canvas_height / element_height * (y - canvas_y)));
+}
+
+static bool cursor_inside_canvas = true;
+
+EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+	// Empty ID is canvas.
+	String target_id = String::utf8(p_event->id);
+	if (target_id.empty() || "#" + target_id == display->canvas_id) {
+		// This event property is the only reliable data on
+		// browser fullscreen state.
+		if (p_event->isFullscreen) {
+			display->window_mode = WINDOW_MODE_FULLSCREEN;
+		} else {
+			display->window_mode = WINDOW_MODE_WINDOWED;
+		}
+	}
+	return false;
+}
+
+// Keys
+
+template <typename T>
+static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) {
+
+	godot_event->set_shift(emscripten_event_ptr->shiftKey);
+	godot_event->set_alt(emscripten_event_ptr->altKey);
+	godot_event->set_control(emscripten_event_ptr->ctrlKey);
+	godot_event->set_metakey(emscripten_event_ptr->metaKey);
+}
+
+static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) {
+
+	Ref<InputEventKey> ev;
+	ev.instance();
+	ev->set_echo(emscripten_event->repeat);
+	dom2godot_mod(emscripten_event, ev);
+	ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode));
+	ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode));
+
+	String unicode = String::utf8(emscripten_event->key);
+	// Check if empty or multi-character (e.g. `CapsLock`).
+	if (unicode.length() != 1) {
+		// Might be empty as well, but better than nonsense.
+		unicode = String::utf8(emscripten_event->charValue);
+	}
+	if (unicode.length() == 1) {
+		ev->set_unicode(unicode[0]);
+	}
+
+	return ev;
+}
+
+EM_BOOL DisplayServerJavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+	Ref<InputEventKey> ev = setup_key_event(p_event);
+	ev->set_pressed(true);
+	if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) {
+		// Defer to keypress event for legacy unicode retrieval.
+		display->deferred_key_event = ev;
+		// Do not suppress keypress event.
+		return false;
+	}
+	Input::get_singleton()->parse_input_event(ev);
+	return true;
+}
+
+EM_BOOL DisplayServerJavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+	display->deferred_key_event->set_unicode(p_event->charCode);
+	Input::get_singleton()->parse_input_event(display->deferred_key_event);
+	return true;
+}
+
+EM_BOOL DisplayServerJavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) {
+
+	Ref<InputEventKey> ev = setup_key_event(p_event);
+	ev->set_pressed(false);
+	Input::get_singleton()->parse_input_event(ev);
+	return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0;
+}
+
+// Mouse
+
+EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+
+	Ref<InputEventMouseButton> ev;
+	ev.instance();
+	ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN);
+	ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY));
+	ev->set_global_position(ev->get_position());
+	dom2godot_mod(p_event, ev);
+
+	switch (p_event->button) {
+		case DOM_BUTTON_LEFT:
+			ev->set_button_index(BUTTON_LEFT);
+			break;
+		case DOM_BUTTON_MIDDLE:
+			ev->set_button_index(BUTTON_MIDDLE);
+			break;
+		case DOM_BUTTON_RIGHT:
+			ev->set_button_index(BUTTON_RIGHT);
+			break;
+		case DOM_BUTTON_XBUTTON1:
+			ev->set_button_index(BUTTON_XBUTTON1);
+			break;
+		case DOM_BUTTON_XBUTTON2:
+			ev->set_button_index(BUTTON_XBUTTON2);
+			break;
+		default:
+			return false;
+	}
+
+	if (ev->is_pressed()) {
+
+		double diff = emscripten_get_now() - display->last_click_ms;
+
+		if (ev->get_button_index() == display->last_click_button_index) {
+
+			if (diff < 400 && Point2(display->last_click_pos).distance_to(ev->get_position()) < 5) {
+
+				display->last_click_ms = 0;
+				display->last_click_pos = Point2(-100, -100);
+				display->last_click_button_index = -1;
+				ev->set_doubleclick(true);
+			}
+
+		} else {
+			display->last_click_button_index = ev->get_button_index();
+		}
+
+		if (!ev->is_doubleclick()) {
+			display->last_click_ms += diff;
+			display->last_click_pos = ev->get_position();
+		}
+	}
+
+	Input *input = Input::get_singleton();
+	int mask = input->get_mouse_button_mask();
+	int button_flag = 1 << (ev->get_button_index() - 1);
+	if (ev->is_pressed()) {
+		// Since the event is consumed, focus manually. The containing iframe,
+		// if exists, may not have focus yet, so focus even if already focused.
+		focus_canvas();
+		mask |= button_flag;
+	} else if (mask & button_flag) {
+		mask &= ~button_flag;
+	} else {
+		// Received release event, but press was outside the canvas, so ignore.
+		return false;
+	}
+	ev->set_button_mask(mask);
+
+	input->parse_input_event(ev);
+	// Prevent multi-click text selection and wheel-click scrolling anchor.
+	// Context menu is prevented through contextmenu event.
+	return true;
+}
+
+EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) {
+
+	Input *input = Input::get_singleton();
+	int input_mask = input->get_mouse_button_mask();
+	Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY);
+	// For motion outside the canvas, only read mouse movement if dragging
+	// started inside the canvas; imitating desktop app behaviour.
+	if (!cursor_inside_canvas && !input_mask)
+		return false;
+
+	Ref<InputEventMouseMotion> ev;
+	ev.instance();
+	dom2godot_mod(p_event, ev);
+	ev->set_button_mask(input_mask);
+
+	ev->set_position(pos);
+	ev->set_global_position(ev->get_position());
+
+	ev->set_relative(Vector2(p_event->movementX, p_event->movementY));
+	input->set_mouse_position(ev->get_position());
+	ev->set_speed(input->get_last_mouse_speed());
+
+	input->parse_input_event(ev);
+	// Don't suppress mouseover/-leave events.
+	return false;
+}
+
+// Cursor
+static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape) {
+
+	switch (p_shape) {
+		case DisplayServer::CURSOR_ARROW:
+			return "auto";
+		case DisplayServer::CURSOR_IBEAM:
+			return "text";
+		case DisplayServer::CURSOR_POINTING_HAND:
+			return "pointer";
+		case DisplayServer::CURSOR_CROSS:
+			return "crosshair";
+		case DisplayServer::CURSOR_WAIT:
+			return "progress";
+		case DisplayServer::CURSOR_BUSY:
+			return "wait";
+		case DisplayServer::CURSOR_DRAG:
+			return "grab";
+		case DisplayServer::CURSOR_CAN_DROP:
+			return "grabbing";
+		case DisplayServer::CURSOR_FORBIDDEN:
+			return "no-drop";
+		case DisplayServer::CURSOR_VSIZE:
+			return "ns-resize";
+		case DisplayServer::CURSOR_HSIZE:
+			return "ew-resize";
+		case DisplayServer::CURSOR_BDIAGSIZE:
+			return "nesw-resize";
+		case DisplayServer::CURSOR_FDIAGSIZE:
+			return "nwse-resize";
+		case DisplayServer::CURSOR_MOVE:
+			return "move";
+		case DisplayServer::CURSOR_VSPLIT:
+			return "row-resize";
+		case DisplayServer::CURSOR_HSPLIT:
+			return "col-resize";
+		case DisplayServer::CURSOR_HELP:
+			return "help";
+		default:
+			return "auto";
+	}
+}
+
+static void set_css_cursor(const char *p_cursor) {
+
+	/* clang-format off */
+	EM_ASM_({
+		Module['canvas'].style.cursor = UTF8ToString($0);
+	}, p_cursor);
+	/* clang-format on */
+}
+
+static bool is_css_cursor_hidden() {
+
+	/* clang-format off */
+	return EM_ASM_INT({
+		return Module['canvas'].style.cursor === 'none';
+	});
+	/* clang-format on */
+}
+
+void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) {
+
+	ERR_FAIL_INDEX(p_shape, CURSOR_MAX);
+
+	if (mouse_get_mode() == MOUSE_MODE_VISIBLE) {
+		if (cursors[p_shape] != "") {
+			Vector<String> url = cursors[p_shape].split("?");
+			set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8());
+		} else {
+			set_css_cursor(godot2dom_cursor(p_shape));
+		}
+	}
+
+	cursor_shape = p_shape;
+}
+
+DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const {
+	return cursor_shape;
+}
+
+void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {
+
+	if (p_cursor.is_valid()) {
+
+		Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape);
+
+		if (cursor_c) {
+			if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) {
+				cursor_set_shape(p_shape);
+				return;
+			}
+
+			cursors_cache.erase(p_shape);
+		}
+
+		Ref<Texture2D> texture = p_cursor;
+		Ref<AtlasTexture> atlas_texture = p_cursor;
+		Ref<Image> image;
+		Size2 texture_size;
+		Rect2 atlas_rect;
+
+		if (texture.is_valid()) {
+			image = texture->get_data();
+		}
+
+		if (!image.is_valid() && atlas_texture.is_valid()) {
+			texture = atlas_texture->get_atlas();
+
+			atlas_rect.size.width = texture->get_width();
+			atlas_rect.size.height = texture->get_height();
+			atlas_rect.position.x = atlas_texture->get_region().position.x;
+			atlas_rect.position.y = atlas_texture->get_region().position.y;
+
+			texture_size.width = atlas_texture->get_region().size.x;
+			texture_size.height = atlas_texture->get_region().size.y;
+		} else if (image.is_valid()) {
+			texture_size.width = texture->get_width();
+			texture_size.height = texture->get_height();
+		}
+
+		ERR_FAIL_COND(!texture.is_valid());
+		ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0);
+		ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256);
+		ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height);
+
+		image = texture->get_data();
+
+		ERR_FAIL_COND(!image.is_valid());
+
+		image = image->duplicate();
+
+		if (atlas_texture.is_valid())
+			image->crop_from_point(
+					atlas_rect.position.x,
+					atlas_rect.position.y,
+					texture_size.width,
+					texture_size.height);
+
+		if (image->get_format() != Image::FORMAT_RGBA8) {
+			image->convert(Image::FORMAT_RGBA8);
+		}
+
+		png_image png_meta;
+		memset(&png_meta, 0, sizeof png_meta);
+		png_meta.version = PNG_IMAGE_VERSION;
+		png_meta.width = texture_size.width;
+		png_meta.height = texture_size.height;
+		png_meta.format = PNG_FORMAT_RGBA;
+
+		PackedByteArray png;
+		size_t len;
+		PackedByteArray data = image->get_data();
+		ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr));
+
+		png.resize(len);
+		ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr));
+
+		char *object_url;
+		/* clang-format off */
+		EM_ASM({
+			var PNG_PTR = $0;
+			var PNG_LEN = $1;
+			var PTR = $2;
+
+			var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' });
+			var url = URL.createObjectURL(png);
+			var length_bytes = lengthBytesUTF8(url) + 1;
+			var string_on_wasm_heap = _malloc(length_bytes);
+			setValue(PTR, string_on_wasm_heap, '*');
+			stringToUTF8(url, string_on_wasm_heap, length_bytes);
+		}, png.ptr(), len, &object_url);
+		/* clang-format on */
+
+		String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y);
+
+		/* clang-format off */
+		EM_ASM({ _free($0); }, object_url);
+		/* clang-format on */
+
+		if (cursors[p_shape] != "") {
+			/* clang-format off */
+			EM_ASM({
+				URL.revokeObjectURL(UTF8ToString($0).split('?')[0]);
+			}, cursors[p_shape].utf8().get_data());
+			/* clang-format on */
+			cursors[p_shape] = "";
+		}
+
+		cursors[p_shape] = url;
+
+		Vector<Variant> params;
+		params.push_back(p_cursor);
+		params.push_back(p_hotspot);
+		cursors_cache.insert(p_shape, params);
+
+	} else if (cursors[p_shape] != "") {
+		/* clang-format off */
+		EM_ASM({
+			URL.revokeObjectURL(UTF8ToString($0).split('?')[0]);
+		}, cursors[p_shape].utf8().get_data());
+		/* clang-format on */
+		cursors[p_shape] = "";
+
+		cursors_cache.erase(p_shape);
+	}
+
+	cursor_set_shape(cursor_shape);
+}
+
+// Mouse mode
+void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) {
+
+	ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform.");
+	if (p_mode == mouse_get_mode())
+		return;
+
+	if (p_mode == MOUSE_MODE_VISIBLE) {
+
+		// set_css_cursor must be called before set_cursor_shape to make the cursor visible
+		set_css_cursor(godot2dom_cursor(cursor_shape));
+		cursor_set_shape(cursor_shape);
+		emscripten_exit_pointerlock();
+
+	} else if (p_mode == MOUSE_MODE_HIDDEN) {
+
+		set_css_cursor("none");
+		emscripten_exit_pointerlock();
+
+	} else if (p_mode == MOUSE_MODE_CAPTURED) {
+
+		EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false);
+		ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback.");
+		ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback.");
+		// set_css_cursor must be called before cursor_set_shape to make the cursor visible
+		set_css_cursor(godot2dom_cursor(cursor_shape));
+		cursor_set_shape(cursor_shape);
+	}
+}
+
+DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const {
+
+	if (is_css_cursor_hidden())
+		return MOUSE_MODE_HIDDEN;
+
+	EmscriptenPointerlockChangeEvent ev;
+	emscripten_get_pointerlock_status(&ev);
+	return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE;
+}
+
+// Wheel
+
+EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) {
+
+	ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false);
+	if (!is_canvas_focused()) {
+		if (cursor_inside_canvas) {
+			focus_canvas();
+		} else {
+			return false;
+		}
+	}
+
+	Input *input = Input::get_singleton();
+	Ref<InputEventMouseButton> ev;
+	ev.instance();
+	ev->set_position(input->get_mouse_position());
+	ev->set_global_position(ev->get_position());
+
+	ev->set_shift(input->is_key_pressed(KEY_SHIFT));
+	ev->set_alt(input->is_key_pressed(KEY_ALT));
+	ev->set_control(input->is_key_pressed(KEY_CONTROL));
+	ev->set_metakey(input->is_key_pressed(KEY_META));
+
+	if (p_event->deltaY < 0)
+		ev->set_button_index(BUTTON_WHEEL_UP);
+	else if (p_event->deltaY > 0)
+		ev->set_button_index(BUTTON_WHEEL_DOWN);
+	else if (p_event->deltaX > 0)
+		ev->set_button_index(BUTTON_WHEEL_LEFT);
+	else if (p_event->deltaX < 0)
+		ev->set_button_index(BUTTON_WHEEL_RIGHT);
+	else
+		return false;
+
+	// Different browsers give wildly different delta values, and we can't
+	// interpret deltaMode, so use default value for wheel events' factor.
+
+	int button_flag = 1 << (ev->get_button_index() - 1);
+
+	ev->set_pressed(true);
+	ev->set_button_mask(input->get_mouse_button_mask() | button_flag);
+	input->parse_input_event(ev);
+
+	ev->set_pressed(false);
+	ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag);
+	input->parse_input_event(ev);
+
+	return true;
+}
+
+// Touch
+EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+	Ref<InputEventScreenTouch> ev;
+	ev.instance();
+	int lowest_id_index = -1;
+	for (int i = 0; i < p_event->numTouches; ++i) {
+
+		const EmscriptenTouchPoint &touch = p_event->touches[i];
+		if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier)
+			lowest_id_index = i;
+		if (!touch.isChanged)
+			continue;
+		ev->set_index(touch.identifier);
+		ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY));
+		display->touches[i] = ev->get_position();
+		ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART);
+
+		Input::get_singleton()->parse_input_event(ev);
+	}
+	// Resume audio context after input in case autoplay was denied.
+	return true;
+}
+
+EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {
+
+	DisplayServerJavaScript *display = get_singleton();
+	Ref<InputEventScreenDrag> ev;
+	ev.instance();
+	int lowest_id_index = -1;
+	for (int i = 0; i < p_event->numTouches; ++i) {
+
+		const EmscriptenTouchPoint &touch = p_event->touches[i];
+		if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier)
+			lowest_id_index = i;
+		if (!touch.isChanged)
+			continue;
+		ev->set_index(touch.identifier);
+		ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY));
+		Point2 &prev = display->touches[i];
+		ev->set_relative(ev->get_position() - prev);
+		prev = ev->get_position();
+
+		Input::get_singleton()->parse_input_event(ev);
+	}
+	return true;
+}
+
+bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const {
+	return EM_ASM_INT({ return 'ontouchstart' in window; });
+}
+
+// Gamepad
+
+EM_BOOL DisplayServerJavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) {
+
+	Input *input = Input::get_singleton();
+	if (p_event_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) {
+
+		String guid = "";
+		if (String::utf8(p_event->mapping) == "standard")
+			guid = "Default HTML5 Gamepad";
+		input->joy_connection_changed(p_event->index, true, String::utf8(p_event->id), guid);
+	} else {
+		input->joy_connection_changed(p_event->index, false, "");
+	}
+	return true;
+}
+
+void DisplayServerJavaScript::process_joypads() {
+
+	int joypad_count = emscripten_get_num_gamepads();
+	Input *input = Input::get_singleton();
+	for (int joypad = 0; joypad < joypad_count; joypad++) {
+		EmscriptenGamepadEvent state;
+		EMSCRIPTEN_RESULT query_result = emscripten_get_gamepad_status(joypad, &state);
+		// Chromium reserves gamepads slots, so NO_DATA is an expected result.
+		ERR_CONTINUE(query_result != EMSCRIPTEN_RESULT_SUCCESS &&
+					 query_result != EMSCRIPTEN_RESULT_NO_DATA);
+		if (query_result == EMSCRIPTEN_RESULT_SUCCESS && state.connected) {
+
+			int button_count = MIN(state.numButtons, 18);
+			int axis_count = MIN(state.numAxes, 8);
+			for (int button = 0; button < button_count; button++) {
+
+				float value = state.analogButton[button];
+				if (String::utf8(state.mapping) == "standard" && (button == JOY_ANALOG_L2 || button == JOY_ANALOG_R2)) {
+					Input::JoyAxis joy_axis;
+					joy_axis.min = 0;
+					joy_axis.value = value;
+					input->joy_axis(joypad, button, joy_axis);
+				} else {
+					input->joy_button(joypad, button, value);
+				}
+			}
+			for (int axis = 0; axis < axis_count; axis++) {
+
+				Input::JoyAxis joy_axis;
+				joy_axis.min = -1;
+				joy_axis.value = state.axis[axis];
+				input->joy_axis(joypad, axis, joy_axis);
+			}
+		}
+	}
+}
+
+#if 0
+bool DisplayServerJavaScript::is_joy_known(int p_device) {
+
+	return Input::get_singleton()->is_joy_mapped(p_device);
+}
+
+String DisplayServerJavaScript::get_joy_guid(int p_device) const {
+
+	return Input::get_singleton()->get_joy_guid_remapped(p_device);
+}
+#endif
+
+Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() {
+	Vector<String> drivers;
+	drivers.push_back("dummy");
+	return drivers;
+}
+
+// Clipboard
+extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) {
+	// Only call set_clipboard from OS (sets local clipboard)
+	DisplayServerJavaScript::get_singleton()->clipboard = p_text;
+}
+
+void DisplayServerJavaScript::clipboard_set(const String &p_text) {
+	/* clang-format off */
+	int err = EM_ASM_INT({
+		var text = UTF8ToString($0);
+		if (!navigator.clipboard || !navigator.clipboard.writeText)
+			return 1;
+		navigator.clipboard.writeText(text).catch(function(e) {
+			// Setting OS clipboard is only possible from an input callback.
+			console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e);
+		});
+		return 0;
+	}, p_text.utf8().get_data());
+	/* clang-format on */
+	ERR_FAIL_COND_MSG(err, "Clipboard API is not supported.");
+}
+
+String DisplayServerJavaScript::clipboard_get() const {
+	/* clang-format off */
+	EM_ASM({
+		try {
+			navigator.clipboard.readText().then(function (result) {
+				ccall('update_clipboard', 'void', ['string'], [result]);
+			}).catch(function (e) {
+				// Fail graciously.
+			});
+		} catch (e) {
+			// Fail graciously.
+		}
+	});
+	/* clang-format on */
+	return clipboard;
+}
+
+extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) {
+
+	if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) {
+		cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER;
+	}
+	OS_JavaScript *os = OS_JavaScript::get_singleton();
+	if (os->is_finalizing())
+		return; // We don't want events anymore.
+	DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton();
+	if (ds && !ds->window_event_callback.is_null()) {
+		Variant event = int(p_notification);
+		Variant *eventp = &event;
+		Variant ret;
+		Callable::CallError ce;
+		ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce);
+	}
+}
+
+void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) {
+
+	/* clang-format off */
+	EM_ASM_({
+		window.alert(UTF8ToString($0));
+	}, p_alert.utf8().get_data());
+	/* clang-format on */
+}
+
+void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) {
+
+	ERR_FAIL_COND(p_icon.is_null());
+	Ref<Image> icon = p_icon;
+	if (icon->is_compressed()) {
+		icon = icon->duplicate();
+		ERR_FAIL_COND(icon->decompress() != OK);
+	}
+	if (icon->get_format() != Image::FORMAT_RGBA8) {
+		if (icon == p_icon)
+			icon = icon->duplicate();
+		icon->convert(Image::FORMAT_RGBA8);
+	}
+
+	png_image png_meta;
+	memset(&png_meta, 0, sizeof png_meta);
+	png_meta.version = PNG_IMAGE_VERSION;
+	png_meta.width = icon->get_width();
+	png_meta.height = icon->get_height();
+	png_meta.format = PNG_FORMAT_RGBA;
+
+	PackedByteArray png;
+	size_t len;
+	PackedByteArray data = icon->get_data();
+	ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr));
+
+	png.resize(len);
+	ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr));
+
+	/* clang-format off */
+	EM_ASM({
+		var PNG_PTR = $0;
+		var PNG_LEN = $1;
+
+		var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" });
+		var url = URL.createObjectURL(png);
+		var link = document.getElementById('-gd-engine-icon');
+		if (link === null) {
+			link = document.createElement('link');
+			link.rel = 'icon';
+			link.id = '-gd-engine-icon';
+			document.head.appendChild(link);
+		}
+		link.href = url;
+	}, png.ptr(), len);
+	/* clang-format on */
+}
+
+void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) {
+	OS_JavaScript *os = OS_JavaScript::get_singleton();
+	if (os->is_finalizing())
+		return; // We don't want events anymore.
+
+	// Resume audio context after input in case autoplay was denied.
+	os->resume_audio();
+
+	Callable cb = get_singleton()->input_event_callback;
+	if (!cb.is_null()) {
+		Variant ev = p_event;
+		Variant *evp = &ev;
+		Variant ret;
+		Callable::CallError ce;
+		cb.call((const Variant **)&evp, 1, ret, ce);
+	}
+}
+
+DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
+	return memnew(DisplayServerJavaScript(p_rendering_driver, p_mode, p_flags, p_resolution, r_error));
+}
+
+DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
+
+	/* clang-format off */
+	EM_ASM({
+		const canvas = Module['canvas'];
+		var enc = new TextEncoder("utf-8");
+		buffer = new Uint8Array(enc.encode(canvas.id));
+		var len = buffer.length*buffer.BYTES_PER_ELEMENT;
+		var out = Module._malloc(len);
+		Module.HEAPU8.set(buffer, out);
+		ccall("_set_canvas_id",
+			"void",
+			["number", "number"],
+			[out, len]
+		);
+		Module._free(out);
+	});
+	/* clang-format on */
+
+	RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu?
+#if 0
+	EmscriptenWebGLContextAttributes attributes;
+	emscripten_webgl_init_context_attributes(&attributes);
+	attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");
+	attributes.antialias = false;
+	ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER);
+
+	if (p_desired.layered) {
+		set_window_per_pixel_transparency_enabled(true);
+	}
+
+	bool gl_initialization_error = false;
+
+	if (RasterizerGLES2::is_viable() == OK) {
+		attributes.majorVersion = 1;
+		RasterizerGLES2::register_config();
+		RasterizerGLES2::make_current();
+	} else {
+		gl_initialization_error = true;
+	}
+
+	EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes);
+	if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
+		gl_initialization_error = true;
+	}
+
+	if (gl_initialization_error) {
+		OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.",
+				"Unable to initialize video driver");
+		return ERR_UNAVAILABLE;
+	}
+
+	video_driver_index = p_video_driver;
+#endif
+
+	/* clang-format off */
+	window_set_mode(p_mode);
+	if (EM_ASM_INT_V({ return Module['resizeCanvasOnStart'] })) {
+		/* clang-format on */
+		window_set_size(p_resolution);
+	}
+
+	EMSCRIPTEN_RESULT result;
+	CharString id = canvas_id.utf8();
+#define EM_CHECK(ev)                         \
+	if (result != EMSCRIPTEN_RESULT_SUCCESS) \
+		ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result));
+#define SET_EM_CALLBACK(target, ev, cb)                                  \
+	result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \
+	EM_CHECK(ev)
+#define SET_EM_CALLBACK_NOTARGET(ev, cb)                         \
+	result = emscripten_set_##ev##_callback(nullptr, true, &cb); \
+	EM_CHECK(ev)
+	// These callbacks from Emscripten's html5.h suffice to access most
+	// JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM
+	// is used below.
+	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback)
+	SET_EM_CALLBACK(id.get_data(), mousedown, mouse_button_callback)
+	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback)
+	SET_EM_CALLBACK(id.get_data(), wheel, wheel_callback)
+	SET_EM_CALLBACK(id.get_data(), touchstart, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), touchmove, touchmove_callback)
+	SET_EM_CALLBACK(id.get_data(), touchend, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), touchcancel, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), keydown, keydown_callback)
+	SET_EM_CALLBACK(id.get_data(), keypress, keypress_callback)
+	SET_EM_CALLBACK(id.get_data(), keyup, keyup_callback)
+	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback)
+	SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback)
+	SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback)
+#undef SET_EM_CALLBACK_NOTARGET
+#undef SET_EM_CALLBACK
+#undef EM_CHECK
+
+	/* clang-format off */
+	EM_ASM_ARGS({
+		Module.listeners = {};
+		const send_window_event = cwrap('send_window_event', null, ['number']);
+		const notifications = arguments;
+		(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) {
+			Module.listeners[event] = send_window_event.bind(null, notifications[index]);
+			Module['canvas'].addEventListener(event, Module.listeners[event]);
+		});
+		// Clipboard
+		const update_clipboard = cwrap('update_clipboard', null, ['string']);
+		Module.listeners['paste'] = function(evt) {
+			update_clipboard(evt.clipboardData.getData('text'));
+		};
+		window.addEventListener('paste', Module.listeners['paste'], false);
+	},
+		WINDOW_EVENT_MOUSE_ENTER,
+		WINDOW_EVENT_MOUSE_EXIT,
+		WINDOW_EVENT_FOCUS_IN,
+		WINDOW_EVENT_FOCUS_OUT
+	);
+	/* clang-format on */
+
+	Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event);
+}
+
+DisplayServerJavaScript::~DisplayServerJavaScript() {
+	EM_ASM({
+		Object.entries(Module.listeners).forEach(function(kv) {
+			if (kv[0] == 'paste') {
+				window.removeEventListener(kv[0], kv[1], true);
+			} else {
+				Module['canvas'].removeEventListener(kv[0], kv[1]);
+			}
+		});
+		Module.listeners = {};
+	});
+	//emscripten_webgl_commit_frame();
+	//emscripten_webgl_destroy_context(webgl_ctx);
+}
+
+bool DisplayServerJavaScript::has_feature(Feature p_feature) const {
+	switch (p_feature) {
+		//case FEATURE_CONSOLE_WINDOW:
+		//case FEATURE_GLOBAL_MENU:
+		//case FEATURE_HIDPI:
+		//case FEATURE_IME:
+		case FEATURE_ICON:
+		case FEATURE_CLIPBOARD:
+		case FEATURE_CURSOR_SHAPE:
+		case FEATURE_CUSTOM_CURSOR_SHAPE:
+		case FEATURE_MOUSE:
+		case FEATURE_TOUCHSCREEN:
+			return true;
+		//case FEATURE_MOUSE_WARP:
+		//case FEATURE_NATIVE_DIALOG:
+		//case FEATURE_NATIVE_ICON:
+		//case FEATURE_NATIVE_VIDEO:
+		//case FEATURE_WINDOW_TRANSPARENCY:
+		//case FEATURE_KEEP_SCREEN_ON:
+		//case FEATURE_ORIENTATION:
+		//case FEATURE_VIRTUAL_KEYBOARD:
+		default:
+			return false;
+	}
+}
+
+void DisplayServerJavaScript::register_javascript_driver() {
+	register_create_function("javascript", create_func, get_rendering_drivers_func);
+}
+
+String DisplayServerJavaScript::get_name() const {
+	return "javascript";
+}
+
+int DisplayServerJavaScript::get_screen_count() const {
+	return 1;
+}
+
+Point2i DisplayServerJavaScript::screen_get_position(int p_screen) const {
+	return Point2i(); // TODO offsetX/Y?
+}
+
+Size2i DisplayServerJavaScript::screen_get_size(int p_screen) const {
+
+	EmscriptenFullscreenChangeEvent ev;
+	EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev);
+	ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2i());
+	return Size2i(ev.screenWidth, ev.screenHeight);
+}
+
+Rect2i DisplayServerJavaScript::screen_get_usable_rect(int p_screen) const {
+	int canvas[2];
+	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
+	return Rect2i(0, 0, canvas[0], canvas[1]);
+}
+
+int DisplayServerJavaScript::screen_get_dpi(int p_screen) const {
+	return 96; // TODO maybe check pixel ratio via window.devicePixelRatio * 96? Inexact.
+}
+
+Vector<DisplayServer::WindowID> DisplayServerJavaScript::get_window_list() const {
+	Vector<WindowID> ret;
+	ret.push_back(MAIN_WINDOW_ID);
+	return ret;
+}
+
+DisplayServerJavaScript::WindowID DisplayServerJavaScript::get_window_at_screen_position(const Point2i &p_position) const {
+	return MAIN_WINDOW_ID;
+}
+
+void DisplayServerJavaScript::window_attach_instance_id(ObjectID p_instance, WindowID p_window) {
+	window_attached_instance_id = p_instance;
+}
+
+ObjectID DisplayServerJavaScript::window_get_attached_instance_id(WindowID p_window) const {
+	return window_attached_instance_id;
+}
+
+void DisplayServerJavaScript::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) {
+	// Not supported.
+}
+
+void DisplayServerJavaScript::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) {
+	window_event_callback = p_callable;
+}
+
+void DisplayServerJavaScript::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) {
+	input_event_callback = p_callable;
+}
+
+void DisplayServerJavaScript::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) {
+	input_text_callback = p_callable; // TODO unused... do I need this?
+}
+
+void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) {
+	// TODO this should be implemented.
+}
+
+void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) {
+	/* clang-format off */
+	EM_ASM_({
+		document.title = UTF8ToString($0);
+	}, p_title.utf8().get_data());
+	/* clang-format on */
+}
+
+int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const {
+	return 1;
+}
+
+void DisplayServerJavaScript::window_set_current_screen(int p_screen, WindowID p_window) {
+	// Not implemented.
+}
+
+Point2i DisplayServerJavaScript::window_get_position(WindowID p_window) const {
+	return Point2i(); // TODO Does this need implementation?
+}
+void DisplayServerJavaScript::window_set_position(const Point2i &p_position, WindowID p_window) {
+	// Not supported.
+}
+
+void DisplayServerJavaScript::window_set_transient(WindowID p_window, WindowID p_parent) {
+	// Not supported.
+}
+
+void DisplayServerJavaScript::window_set_max_size(const Size2i p_size, WindowID p_window) {
+	// Not supported.
+}
+
+Size2i DisplayServerJavaScript::window_get_max_size(WindowID p_window) const {
+	return Size2i();
+}
+
+void DisplayServerJavaScript::window_set_min_size(const Size2i p_size, WindowID p_window) {
+	// Not supported.
+}
+
+Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const {
+	return Size2i();
+}
+
+void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) {
+	emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y);
+}
+
+Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const {
+	int canvas[2];
+	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
+	return Size2(canvas[0], canvas[1]);
+}
+
+Size2i DisplayServerJavaScript::window_get_real_size(WindowID p_window) const {
+	return window_get_size(p_window);
+}
+
+void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_window) {
+	if (window_mode == p_mode)
+		return;
+
+	switch (p_mode) {
+		case WINDOW_MODE_WINDOWED: {
+			if (window_mode == WINDOW_MODE_FULLSCREEN) {
+				emscripten_exit_fullscreen();
+			}
+			window_mode = WINDOW_MODE_WINDOWED;
+			window_set_size(windowed_size);
+		} break;
+		case WINDOW_MODE_FULLSCREEN: {
+			EmscriptenFullscreenStrategy strategy;
+			strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
+			strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
+			strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
+			strategy.canvasResizedCallback = nullptr;
+			EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id.utf8().get_data(), false, &strategy);
+			ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
+			ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
+		} break;
+		case WINDOW_MODE_MAXIMIZED:
+		case WINDOW_MODE_MINIMIZED:
+			WARN_PRINT("WindowMode MAXIMIZED and MINIMIZED are not supported in HTML5 platform.");
+			break;
+		default:
+			break;
+	}
+}
+
+DisplayServerJavaScript::WindowMode DisplayServerJavaScript::window_get_mode(WindowID p_window) const {
+	return window_mode;
+}
+
+bool DisplayServerJavaScript::window_is_maximize_allowed(WindowID p_window) const {
+	return false;
+}
+
+void DisplayServerJavaScript::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) {
+	// Not supported.
+}
+
+bool DisplayServerJavaScript::window_get_flag(WindowFlags p_flag, WindowID p_window) const {
+	return false;
+}
+
+void DisplayServerJavaScript::window_request_attention(WindowID p_window) {
+	// Not supported.
+}
+
+void DisplayServerJavaScript::window_move_to_foreground(WindowID p_window) {
+	// Not supported.
+}
+
+bool DisplayServerJavaScript::window_can_draw(WindowID p_window) const {
+	return true;
+}
+
+bool DisplayServerJavaScript::can_any_window_draw() const {
+	return true;
+}
+
+void DisplayServerJavaScript::process_events() {
+	if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS)
+		process_joypads();
+}
+
+int DisplayServerJavaScript::get_current_video_driver() const {
+	return 1;
+}
+
+void DisplayServerJavaScript::swap_buffers() {
+	//emscripten_webgl_commit_frame();
+}

+ 187 - 0
platform/javascript/display_server_javascript.h

@@ -0,0 +1,187 @@
+/*************************************************************************/
+/*  display_server_javascript.h                                          */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef DISPLAY_SERVER_JAVASCRIPT_H
+#define DISPLAY_SERVER_JAVASCRIPT_H
+
+#include "servers/display_server.h"
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+class DisplayServerJavaScript : public DisplayServer {
+
+	//int video_driver_index;
+
+	Vector2 windowed_size;
+
+	ObjectID window_attached_instance_id = {};
+
+	Ref<InputEventKey> deferred_key_event;
+	CursorShape cursor_shape = CURSOR_ARROW;
+	String cursors[CURSOR_MAX];
+	Map<CursorShape, Vector<Variant>> cursors_cache;
+	Point2 touches[32];
+
+	Point2i last_click_pos = Point2(-100, -100); // TODO check this again.
+	double last_click_ms = 0;
+	int last_click_button_index = -1;
+
+	static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data);
+
+	static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
+	static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
+	static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
+
+	static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
+	static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
+
+	static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data);
+
+	static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data);
+	static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data);
+
+	static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data);
+	void process_joypads();
+
+	static Vector<String> get_rendering_drivers_func();
+	static DisplayServer *create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error);
+
+	static void _dispatch_input_event(const Ref<InputEvent> &p_event);
+
+protected:
+	virtual int get_current_video_driver() const;
+
+public:
+	// Override return type to make writing static callbacks less tedious.
+	static DisplayServerJavaScript *get_singleton();
+
+	WindowMode window_mode = WINDOW_MODE_WINDOWED;
+
+	String clipboard;
+	String canvas_id;
+
+	Callable window_event_callback;
+	Callable input_event_callback;
+	Callable input_text_callback;
+
+	// from DisplayServer
+	virtual void alert(const String &p_alert, const String &p_title = "ALERT!");
+	virtual bool has_feature(Feature p_feature) const;
+	virtual String get_name() const;
+
+	// cursor
+	virtual void cursor_set_shape(CursorShape p_shape);
+	virtual CursorShape cursor_get_shape() const;
+	virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2());
+
+	// mouse
+	virtual void mouse_set_mode(MouseMode p_mode);
+	virtual MouseMode mouse_get_mode() const;
+
+	// touch
+	virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const;
+
+	// clipboard
+	virtual void clipboard_set(const String &p_text);
+	virtual String clipboard_get() const;
+
+	// screen
+	virtual int get_screen_count() const;
+	virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const;
+	virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const;
+	virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const;
+	virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const;
+
+	// windows
+	virtual Vector<DisplayServer::WindowID> get_window_list() const;
+	virtual WindowID get_window_at_screen_position(const Point2i &p_position) const;
+
+	virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID);
+	virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID);
+	virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID);
+	virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const;
+	virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const;
+	virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual void window_set_transient(WindowID p_window, WindowID p_parent);
+
+	virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID);
+	virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID);
+	virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID);
+	virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const;
+	virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const; // FIXME: Find clearer name for this.
+
+	virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID);
+	virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID);
+	virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID);
+	virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID);
+
+	virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const;
+
+	virtual bool can_any_window_draw() const;
+
+	// events
+	virtual void process_events();
+
+	// icon
+	virtual void set_icon(const Ref<Image> &p_icon);
+
+	// others
+	virtual void swap_buffers();
+
+	static void register_javascript_driver();
+	DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error);
+	~DisplayServerJavaScript();
+};
+
+#endif // DISPLAY_SERVER_JAVASCRIPT_H

+ 9 - 0
platform/javascript/engine/engine.js

@@ -31,6 +31,7 @@ Function('return this')()['Engine'] = (function() {
 		this.rtenv = null;
 		this.customLocale = null;
 		this.resizeCanvasOnStart = false;
+		this.onExecute = null;
 		this.onExit = null;
 	};
 
@@ -115,6 +116,7 @@ Function('return this')()['Engine'] = (function() {
 			me.rtenv['thisProgram'] = me.executableName;
 			me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart;
 			me.rtenv['noExitRuntime'] = true;
+			me.rtenv['onExecute'] = me.onExecute;
 			me.rtenv['onExit'] = function(code) {
 				if (me.onExit)
 					me.onExit(code);
@@ -201,6 +203,12 @@ Function('return this')()['Engine'] = (function() {
 		stderr = printErr;
 	};
 
+	Engine.prototype.setOnExecute = function(onExecute) {
+		if (this.rtenv)
+			this.rtenv.onExecute = onExecute;
+		this.onExecute = onExecute;
+	}
+
 	Engine.prototype.setOnExit = function(onExit) {
 		this.onExit = onExit;
 	}
@@ -230,6 +238,7 @@ Function('return this')()['Engine'] = (function() {
 	Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc;
 	Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc;
 	Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc;
+	Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute;
 	Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
 	Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
 	return Engine;

+ 43 - 6
platform/javascript/javascript_main.cpp

@@ -34,22 +34,63 @@
 
 #include <emscripten/emscripten.h>
 
+static OS_JavaScript *os = nullptr;
+
+void exit_callback() {
+	emscripten_cancel_main_loop(); // After this, we can exit!
+	Main::cleanup();
+	int exit_code = OS_JavaScript::get_singleton()->get_exit_code();
+	memdelete(os);
+	os = nullptr;
+	emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing.
+}
+
+void main_loop_callback() {
+
+	if (os->main_loop_iterate()) {
+		emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async.
+		EM_ASM({
+			// This will contain the list of operations that need to complete before cleanup.
+			Module.async_finish = [];
+		});
+		os->get_main_loop()->finish();
+		os->finalize_async(); // Will add all the async finish functions.
+		EM_ASM({
+			Promise.all(Module.async_finish).then(function() {
+				Module.async_finish = [];
+				ccall("cleanup_after_sync", null, []);
+			});
+		});
+	}
+}
+
+extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() {
+	emscripten_set_main_loop(exit_callback, -1, false);
+}
+
 extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) {
 
 	String idbfs_err = String::utf8(p_idbfs_err);
 	if (!idbfs_err.empty()) {
 		print_line("IndexedDB not available: " + idbfs_err);
 	}
-	OS_JavaScript *os = OS_JavaScript::get_singleton();
 	os->set_idb_available(idbfs_err.empty());
+	// TODO: Check error return value.
+	Main::setup2(); // Manual second phase.
 	// Ease up compatibility.
 	ResourceLoader::set_abort_on_missing_resources(false);
 	Main::start();
-	os->run_async();
+	os->get_main_loop()->init();
+	emscripten_resume_main_loop();
 }
 
 int main(int argc, char *argv[]) {
 
+	os = new OS_JavaScript();
+	Main::setup(argv[0], argc - 1, &argv[1], false);
+	emscripten_set_main_loop(main_loop_callback, -1, false);
+	emscripten_pause_main_loop(); // Will need to wait for FS sync.
+
 	// Sync from persistent state into memory and then
 	// run the 'main_after_fs_sync' function.
 	/* clang-format off */
@@ -62,10 +103,6 @@ int main(int argc, char *argv[]) {
 	);
 	/* clang-format on */
 
-	new OS_JavaScript(argc, argv);
-	// TODO: Check error return value.
-	Main::setup(argv[0], argc - 1, &argv[1]);
-
 	return 0;
 	// Continued async in main_after_fs_sync() from the syncfs() callback.
 }

File diff suppressed because it is too large
+ 8 - 976
platform/javascript/os_javascript.cpp


+ 15 - 81
platform/javascript/os_javascript.h

@@ -35,64 +35,25 @@
 #include "core/input/input.h"
 #include "drivers/unix/os_unix.h"
 #include "servers/audio_server.h"
-#include "servers/rendering/rasterizer.h"
 
 #include <emscripten/html5.h>
 
 class OS_JavaScript : public OS_Unix {
 
-	VideoMode video_mode;
-	Vector2 windowed_size;
-	bool window_maximized;
-	bool entering_fullscreen;
-	bool just_exited_fullscreen;
-	bool transparency_enabled;
-
-	InputDefault *input;
-	Ref<InputEventKey> deferred_key_event;
-	CursorShape cursor_shape;
-	String cursors[CURSOR_MAX];
-	Map<CursorShape, Vector<Variant>> cursors_cache;
-	Point2 touches[32];
-
-	Point2i last_click_pos;
-	double last_click_ms;
-	int last_click_button_index;
-
-	MainLoop *main_loop;
-	int video_driver_index;
+	MainLoop *main_loop = nullptr;
 	AudioDriverJavaScript audio_driver_javascript;
 
-	bool idb_available;
-	int64_t sync_wait_time;
-	int64_t last_sync_check_time;
-
-	static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data);
-
-	static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
-	static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
-	static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
-
-	static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
-	static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
-
-	static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data);
-
-	static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data);
-	static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data);
-
-	static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data);
-	void process_joypads();
+	bool finalizing = false;
+	bool idb_available = false;
+	int64_t sync_wait_time = -1;
+	int64_t last_sync_check_time = -1;
 
 	static void main_loop_callback();
 
 	static void file_access_close_callback(const String &p_file, int p_flags);
 
 protected:
-	virtual int get_current_video_driver() const;
-
-	virtual void initialize_core();
-	virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver);
+	virtual void initialize();
 
 	virtual void set_main_loop(MainLoop *p_main_loop);
 	virtual void delete_main_loop();
@@ -105,65 +66,38 @@ public:
 	// Override return type to make writing static callbacks less tedious.
 	static OS_JavaScript *get_singleton();
 
-	virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);
-	virtual VideoMode get_video_mode(int p_screen = 0) const;
-	virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
-
-	virtual void set_window_size(const Size2);
-	virtual Size2 get_window_size() const;
-	virtual void set_window_maximized(bool p_enabled);
-	virtual bool is_window_maximized() const;
-	virtual void set_window_fullscreen(bool p_enabled);
-	virtual bool is_window_fullscreen() const;
-	virtual Size2 get_screen_size(int p_screen = -1) const;
-
-	virtual Point2 get_mouse_position() const;
-	virtual int get_mouse_button_state() const;
-	virtual void set_cursor_shape(CursorShape p_shape);
-	virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot);
-	virtual void set_mouse_mode(MouseMode p_mode);
-	virtual MouseMode get_mouse_mode() const;
-
-	virtual bool get_window_per_pixel_transparency_enabled() const;
-	virtual void set_window_per_pixel_transparency_enabled(bool p_enabled);
+	virtual void initialize_joypads();
 
 	virtual bool has_touchscreen_ui_hint() const;
 
-	virtual bool is_joy_known(int p_device);
-	virtual String get_joy_guid(int p_device) const;
-
-	virtual int get_video_driver_count() const;
-	virtual const char *get_video_driver_name(int p_driver) const;
-
 	virtual int get_audio_driver_count() const;
 	virtual const char *get_audio_driver_name(int p_driver) const;
 
-	virtual void set_clipboard(const String &p_text);
-	virtual String get_clipboard() const;
-
 	virtual MainLoop *get_main_loop() const;
-	void run_async();
+	void finalize_async();
 	bool main_loop_iterate();
 
 	virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr);
 	virtual Error kill(const ProcessID &p_pid);
 	virtual int get_process_id() const;
 
-	virtual void alert(const String &p_alert, const String &p_title = "ALERT!");
-	virtual void set_window_title(const String &p_title);
-	virtual void set_icon(const Ref<Image> &p_icon);
 	String get_executable_path() const;
 	virtual Error shell_open(String p_uri);
 	virtual String get_name() const;
 	virtual bool can_draw() const;
 
-	virtual String get_resource_dir() const;
+	virtual String get_cache_path() const;
+	virtual String get_config_path() const;
+	virtual String get_data_path() const;
 	virtual String get_user_data_dir() const;
 
 	void set_idb_available(bool p_idb_available);
 	virtual bool is_userfs_persistent() const;
 
-	OS_JavaScript(int p_argc, char *p_argv[]);
+	void resume_audio();
+	bool is_finalizing() { return finalizing; }
+
+	OS_JavaScript();
 };
 
 #endif

Some files were not shown because too many files changed in this diff