| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460 | /*************************************************************************//*  os_javascript.cpp                                                    *//*************************************************************************//*                       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.                *//*************************************************************************/#include "os_javascript.h"#include "core/io/file_access_buffered_fa.h"#include "core/io/json.h"#include "drivers/gles2/rasterizer_gles2.h"#include "drivers/gles3/rasterizer_gles3.h"#include "drivers/unix/dir_access_unix.h"#include "drivers/unix/file_access_unix.h"#include "main/main.h"#include "servers/visual/visual_server_raster.h"#ifndef NO_THREADS#include "servers/visual/visual_server_wrap_mt.h"#endif#include <emscripten.h>#include <png.h>#include <stdlib.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// Window (canvas)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({		return document.activeElement == Module['canvas'];	});	/* clang-format on */}static Point2 compute_position_in_canvas(int x, int y) {	OS_JavaScript *os = OS_JavaScript::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(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);	double element_width;	double element_height;	emscripten_get_element_css_size(os->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)));}bool OS_JavaScript::check_size_force_redraw() {	int canvas_width;	int canvas_height;	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), &canvas_width, &canvas_height);	if (last_width != canvas_width || last_height != canvas_height) {		last_width = canvas_width;		last_height = canvas_height;		// Update the framebuffer size and for redraw.		emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), canvas_width, canvas_height);		return true;	}	return false;}static bool cursor_inside_canvas = true;EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {	OS_JavaScript *os = get_singleton();	// Empty ID is canvas.	String target_id = String::utf8(p_event->id);	if (target_id.empty() || target_id == "canvas") {		// This event property is the only reliable data on		// browser fullscreen state.		os->video_mode.fullscreen = p_event->isFullscreen;		if (os->video_mode.fullscreen) {			os->entering_fullscreen = false;		} else {			// Restoring maximized window now will cause issues,			// so delay until main_loop_iterate.			os->just_exited_fullscreen = true;		}	}	return false;}void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) {	video_mode = p_video_mode;}OS::VideoMode OS_JavaScript::get_video_mode(int p_screen) const {	return video_mode;}Size2 OS_JavaScript::get_screen_size(int p_screen) const {	EmscriptenFullscreenChangeEvent ev;	EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev);	ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2());	return Size2(ev.screenWidth, ev.screenHeight);}void OS_JavaScript::set_window_size(const Size2 p_size) {	windowed_size = p_size;	if (video_mode.fullscreen) {		window_maximized = false;		set_window_fullscreen(false);	} else {		if (window_maximized) {			emscripten_exit_soft_fullscreen();			window_maximized = false;		}		emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y);	}}Size2 OS_JavaScript::get_window_size() const {	int canvas[2];	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);	return Size2(canvas[0], canvas[1]);}void OS_JavaScript::set_window_maximized(bool p_enabled) {	if (video_mode.fullscreen) {		window_maximized = p_enabled;		set_window_fullscreen(false);	} else if (!p_enabled) {		emscripten_exit_soft_fullscreen();		window_maximized = false;	} else if (!window_maximized) {		// Prevent calling emscripten_enter_soft_fullscreen mutltiple times,		// this would hide page elements permanently.		EmscriptenFullscreenStrategy strategy;		strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;		strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;		strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;		strategy.canvasResizedCallback = NULL;		emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);		window_maximized = p_enabled;	}}bool OS_JavaScript::is_window_maximized() const {	return window_maximized;}void OS_JavaScript::set_window_fullscreen(bool p_enabled) {	if (p_enabled == video_mode.fullscreen) {		return;	}	// Just request changes here, if successful, logic continues in	// fullscreen_change_callback.	if (p_enabled) {		if (window_maximized) {			// Soft fullsreen during real fullscreen can cause issues, so exit.			// This must be called before requesting full screen.			emscripten_exit_soft_fullscreen();		}		EmscriptenFullscreenStrategy strategy;		strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;		strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;		strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;		strategy.canvasResizedCallback = NULL;		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.");		// Not fullscreen yet, so prevent "windowed" canvas dimensions from		// being overwritten.		entering_fullscreen = true;	} else {		// No logic allowed here, since exiting w/ ESC key won't use this function.		ERR_FAIL_COND(emscripten_exit_fullscreen() != EMSCRIPTEN_RESULT_SUCCESS);	}}bool OS_JavaScript::is_window_fullscreen() const {	return video_mode.fullscreen;}void OS_JavaScript::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const {	Size2 screen = get_screen_size();	p_list->push_back(OS::VideoMode(screen.width, screen.height, true));}bool OS_JavaScript::get_window_per_pixel_transparency_enabled() const {	if (!is_layered_allowed()) {		return false;	}	return transparency_enabled;}void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) {	if (!is_layered_allowed()) {		return;	}	transparency_enabled = p_enabled;}// Keystemplate <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_scancode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key));	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 OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) {	OS_JavaScript *os = get_singleton();	Ref<InputEventKey> ev = setup_key_event(p_event);	ev->set_pressed(true);	if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_scancode())) {		// Defer to keypress event for legacy unicode retrieval.		os->deferred_key_event = ev;		// Do not suppress keypress event.		return false;	}	os->input->parse_input_event(ev);	// Resume audio context after input in case autoplay was denied.	os->resume_audio();	return true;}EM_BOOL OS_JavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) {	OS_JavaScript *os = get_singleton();	os->deferred_key_event->set_unicode(p_event->charCode);	os->input->parse_input_event(os->deferred_key_event);	return true;}EM_BOOL OS_JavaScript::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);	get_singleton()->input->parse_input_event(ev);	return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0;}// MousePoint2 OS_JavaScript::get_mouse_position() const {	return input->get_mouse_position();}int OS_JavaScript::get_mouse_button_state() const {	return input->get_mouse_button_mask();}EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) {	OS_JavaScript *os = 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() - os->last_click_ms;		if (ev->get_button_index() == os->last_click_button_index) {			if (diff < 400 && Point2(os->last_click_pos).distance_to(ev->get_position()) < 5) {				os->last_click_ms = 0;				os->last_click_pos = Point2(-100, -100);				os->last_click_button_index = -1;				ev->set_doubleclick(true);			}		} else {			os->last_click_button_index = ev->get_button_index();		}		if (!ev->is_doubleclick()) {			os->last_click_ms += diff;			os->last_click_pos = ev->get_position();		}	}	int mask = os->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);	os->input->parse_input_event(ev);	// Resume audio context after input in case autoplay was denied.	os->resume_audio();	// Prevent multi-click text selection and wheel-click scrolling anchor.	// Context menu is prevented through contextmenu event.	return true;}EM_BOOL OS_JavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) {	OS_JavaScript *os = get_singleton();	int input_mask = os->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));	os->input->set_mouse_position(ev->get_position());	ev->set_speed(os->input->get_last_mouse_speed());	os->input->parse_input_event(ev);	// Don't suppress mouseover/-leave events.	return false;}static const char *godot2dom_cursor(OS::CursorShape p_shape) {	switch (p_shape) {		case OS::CURSOR_ARROW:		default:			return "auto";		case OS::CURSOR_IBEAM: return "text";		case OS::CURSOR_POINTING_HAND: return "pointer";		case OS::CURSOR_CROSS: return "crosshair";		case OS::CURSOR_WAIT: return "progress";		case OS::CURSOR_BUSY: return "wait";		case OS::CURSOR_DRAG: return "grab";		case OS::CURSOR_CAN_DROP: return "grabbing";		case OS::CURSOR_FORBIDDEN: return "no-drop";		case OS::CURSOR_VSIZE: return "ns-resize";		case OS::CURSOR_HSIZE: return "ew-resize";		case OS::CURSOR_BDIAGSIZE: return "nesw-resize";		case OS::CURSOR_FDIAGSIZE: return "nwse-resize";		case OS::CURSOR_MOVE: return "move";		case OS::CURSOR_VSPLIT: return "row-resize";		case OS::CURSOR_HSPLIT: return "col-resize";		case OS::CURSOR_HELP: return "help";	}}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 OS_JavaScript::set_cursor_shape(CursorShape p_shape) {	ERR_FAIL_INDEX(p_shape, CURSOR_MAX);	if (get_mouse_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;}void OS_JavaScript::set_custom_mouse_cursor(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) {				set_cursor_shape(p_shape);				return;			}			cursors_cache.erase(p_shape);		}		Ref<Texture> 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()) {				image->duplicate();			}		}		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;		PoolByteArray png;		size_t len;		PoolByteArray::Read r = image->get_data().read();		ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL));		png.resize(len);		PoolByteArray::Write w = png.write();		ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL));		w = PoolByteArray::Write();		r = png.read();		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);		}, r.ptr(), len, &object_url);		/* clang-format on */		r = PoolByteArray::Read();		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);	}	set_cursor_shape(cursor_shape);}void OS_JavaScript::set_mouse_mode(OS::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 == get_mouse_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));		set_cursor_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 set_cursor_shape to make the cursor visible		set_css_cursor(godot2dom_cursor(cursor_shape));		set_cursor_shape(cursor_shape);	}}OS::MouseMode OS_JavaScript::get_mouse_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;}// WheelEM_BOOL OS_JavaScript::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;		}	}	InputDefault *input = get_singleton()->input;	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;}// Touchbool OS_JavaScript::has_touchscreen_ui_hint() const {	/* clang-format off */	return EM_ASM_INT({		return 'ontouchstart' in window;	});	/* clang-format on */}EM_BOOL OS_JavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {	OS_JavaScript *os = 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));		os->touches[i] = ev->get_position();		ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART);		os->input->parse_input_event(ev);	}	// Resume audio context after input in case autoplay was denied.	os->resume_audio();	return true;}EM_BOOL OS_JavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {	OS_JavaScript *os = 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 = os->touches[i];		ev->set_relative(ev->get_position() - prev);		prev = ev->get_position();		os->input->parse_input_event(ev);	}	return true;}// GamepadEM_BOOL OS_JavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) {	InputDefault *input = get_singleton()->input;	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 OS_JavaScript::process_joypads() {	int joypad_count = emscripten_get_num_gamepads();	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)) {					InputDefault::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++) {				InputDefault::JoyAxis joy_axis;				joy_axis.min = -1;				joy_axis.value = state.axis[axis];				input->joy_axis(joypad, axis, joy_axis);			}		}	}}bool OS_JavaScript::is_joy_known(int p_device) {	return input->is_joy_mapped(p_device);}String OS_JavaScript::get_joy_guid(int p_device) const {	return input->get_joy_guid_remapped(p_device);}// Videoint OS_JavaScript::get_video_driver_count() const {	return VIDEO_DRIVER_MAX;}const char *OS_JavaScript::get_video_driver_name(int p_driver) const {	switch (p_driver) {		case VIDEO_DRIVER_GLES3:			return "GLES3";		case VIDEO_DRIVER_GLES2:			return "GLES2";	}	ERR_FAIL_V_MSG(NULL, "Invalid video driver index: " + itos(p_driver) + ".");}// Audioint OS_JavaScript::get_audio_driver_count() const {	return 1;}const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {	return "JavaScript";}// Clipboardextern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) {	// Only call set_clipboard from OS (sets local clipboard)	OS::get_singleton()->OS::set_clipboard(p_text);}void OS_JavaScript::set_clipboard(const String &p_text) {	OS::set_clipboard(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 OS_JavaScript::get_clipboard() 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 this->OS::get_clipboard();}// Lifecycleint OS_JavaScript::get_current_video_driver() const {	return video_driver_index;}void OS_JavaScript::initialize_core() {	OS_Unix::initialize_core();	FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix> >(FileAccess::ACCESS_RESOURCES);}Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {	/* clang-format off */	swap_ok_cancel = EM_ASM_INT({		const win = (['Windows', 'Win64', 'Win32', 'WinCE']);		const plat = navigator.platform || "";		if (win.indexOf(plat) !== -1) {			return 1;		}		return 0;	}) == 1;	/* clang-format on */	EmscriptenWebGLContextAttributes attributes;	emscripten_webgl_init_context_attributes(&attributes);	attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");	attributes.antialias = false;	attributes.explicitSwapControl = true;	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 gles3 = true;	if (p_video_driver == VIDEO_DRIVER_GLES2) {		gles3 = false;	}	bool gl_initialization_error = false;	while (true) {		if (gles3) {			if (RasterizerGLES3::is_viable() == OK) {				attributes.majorVersion = 2;				RasterizerGLES3::register_config();				RasterizerGLES3::make_current();				break;			} else {				if (GLOBAL_GET("rendering/quality/driver/fallback_to_gles2")) {					p_video_driver = VIDEO_DRIVER_GLES2;					gles3 = false;					continue;				} else {					gl_initialization_error = true;					break;				}			}		} else {			if (RasterizerGLES2::is_viable() == OK) {				attributes.majorVersion = 1;				RasterizerGLES2::register_config();				RasterizerGLES2::make_current();				break;			} else {				gl_initialization_error = true;				break;			}		}	}	webgl_ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes);	if (emscripten_webgl_make_context_current(webgl_ctx) != EMSCRIPTEN_RESULT_SUCCESS) {		gl_initialization_error = true;	}	if (gl_initialization_error) {		OS::get_singleton()->alert("Your browser does not support any of the supported WebGL versions.\n"								   "Please update your browser version.",				"Unable to initialize Video driver");		return ERR_UNAVAILABLE;	}	video_driver_index = p_video_driver;	video_mode = p_desired;	// fullscreen_change_callback will correct this if the request is successful.	video_mode.fullscreen = false;	// Emscripten only attempts fullscreen requests if the user input callback	// was registered through one its own functions, so request manually for	// start-up fullscreen.	if (p_desired.fullscreen) {		/* clang-format off */		EM_ASM({			const canvas = Module['canvas'];			(canvas.requestFullscreen || canvas.msRequestFullscreen ||				canvas.mozRequestFullScreen || canvas.mozRequestFullscreen ||				canvas.webkitRequestFullscreen			).call(canvas);		});		/* clang-format on */	}	/* clang-format off */	if (EM_ASM_INT({ return Module['resizeCanvasOnStart'] })) {		/* clang-format on */		set_window_size(Size2(video_mode.width, video_mode.height));	} else {		set_window_size(get_window_size());	}	AudioDriverManager::initialize(p_audio_driver);	visual_server = memnew(VisualServerRaster());#ifndef NO_THREADS	visual_server = memnew(VisualServerWrapMT(visual_server, false));#endif	input = memnew(InputDefault);	EMSCRIPTEN_RESULT result;	CharString id = canvas_id.utf8().get_data();#define EM_CHECK(ev)                         \	if (result != EMSCRIPTEN_RESULT_SUCCESS) \	ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result))#define SET_EM_CALLBACK(target, ev, cb)                               \	result = emscripten_set_##ev##_callback(target, NULL, true, &cb); \	EM_CHECK(ev)#define SET_EM_CALLBACK_NOTARGET(ev, cb)                      \	result = emscripten_set_##ev##_callback(NULL, 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({		Module.listeners = {};		const canvas = Module['canvas'];		const send_notification = cwrap('send_notification', null, ['number']);		const notifications = arguments;		(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) {			Module.listeners[event] = send_notification.bind(null, notifications[index]);			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'], true);		Module.listeners['dragover'] = function(ev) {			// Prevent default behavior (which would try to open the file(s))			ev.preventDefault();		};		// Drag an drop		Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js		canvas.addEventListener('dragover', Module.listeners['dragover'], false);		canvas.addEventListener('drop', Module.listeners['drop'], false);		// Quit request		Module['request_quit'] = function() {			send_notification(notifications[notifications.length - 1]);		};	},		MainLoop::NOTIFICATION_WM_MOUSE_ENTER,		MainLoop::NOTIFICATION_WM_MOUSE_EXIT,		MainLoop::NOTIFICATION_WM_FOCUS_IN,		MainLoop::NOTIFICATION_WM_FOCUS_OUT,		MainLoop::NOTIFICATION_WM_QUIT_REQUEST	);	/* clang-format on */	visual_server->init();	return OK;}bool OS_JavaScript::get_swap_ok_cancel() {	return swap_ok_cancel;}void OS_JavaScript::swap_buffers() {	emscripten_webgl_commit_frame();}void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {	main_loop = p_main_loop;	input->set_main_loop(p_main_loop);}MainLoop *OS_JavaScript::get_main_loop() const {	return main_loop;}void OS_JavaScript::resume_audio() {	if (audio_driver_javascript) {		audio_driver_javascript->resume();	}}bool OS_JavaScript::main_loop_iterate() {	if (is_userfs_persistent() && sync_wait_time >= 0) {		int64_t current_time = get_ticks_msec();		int64_t elapsed_time = current_time - last_sync_check_time;		last_sync_check_time = current_time;		sync_wait_time -= elapsed_time;		if (sync_wait_time < 0) {			/* clang-format off */			EM_ASM(				FS.syncfs(function(error) {					if (error) { err('Failed to save IDB file system: ' + error.message); }				});			);			/* clang-format on */		}	}	if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS)		process_joypads();	if (just_exited_fullscreen) {		if (window_maximized) {			EmscriptenFullscreenStrategy strategy;			strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;			strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;			strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;			strategy.canvasResizedCallback = NULL;			emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);		} else {			emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);		}		emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);		just_exited_fullscreen = false;	}	int canvas[2];	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);	video_mode.width = canvas[0];	video_mode.height = canvas[1];	if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) {		windowed_size.width = canvas[0];		windowed_size.height = canvas[1];	}	return Main::iteration();}void OS_JavaScript::delete_main_loop() {	memdelete(main_loop);}void OS_JavaScript::finalize_async() {	EM_ASM({		const canvas = Module['canvas'];		Object.entries(Module.listeners).forEach(function(kv) {			if (kv[0] == 'paste') {				window.removeEventListener(kv[0], kv[1], true);			} else {				canvas.removeEventListener(kv[0], kv[1]);			}		});		Module.listeners = {};	});	if (audio_driver_javascript) {		audio_driver_javascript->finish_async();	}}void OS_JavaScript::finalize() {	memdelete(input);	visual_server->finish();	emscripten_webgl_commit_frame();	memdelete(visual_server);	emscripten_webgl_destroy_context(webgl_ctx);	if (audio_driver_javascript) {		memdelete(audio_driver_javascript);	}}// MiscellaneousError OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) {	Array args;	for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) {		args.push_back(E->get());	}	String json_args = JSON::print(args);	/* clang-format off */	int failed = EM_ASM_INT({		const json_args = UTF8ToString($0);		const args = JSON.parse(json_args);		if (Module["onExecute"]) {			Module["onExecute"](args);			return 0;		}		return 1;	}, json_args.utf8().get_data());	/* clang-format on */	ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in Javascript via 'engine.setOnExecute' if required.");	return OK;}Error OS_JavaScript::kill(const ProcessID &p_pid) {	ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::kill() is not available on the HTML5 platform.");}int OS_JavaScript::get_process_id() const {	ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform.");}extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) {	if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {		cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;	}	MainLoop *loop = OS_JavaScript::get_singleton()->get_main_loop();	if (loop)		loop->notification(p_notification);}bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {	if (p_feature == "HTML5" || p_feature == "web")		return true;#ifdef JAVASCRIPT_EVAL_ENABLED	if (p_feature == "JavaScript")		return true;#endif	return false;}void OS_JavaScript::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 OS_JavaScript::set_window_title(const String &p_title) {	/* clang-format off */	EM_ASM({		document.title = UTF8ToString($0);	}, p_title.utf8().get_data());	/* clang-format on */}void OS_JavaScript::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;	PoolByteArray png;	size_t len;	PoolByteArray::Read r = icon->get_data().read();	ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL));	png.resize(len);	PoolByteArray::Write w = png.write();	ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL));	w = PoolByteArray::Write();	r = png.read();	/* 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;	}, r.ptr(), len);	/* clang-format on */}String OS_JavaScript::get_executable_path() const {	return OS::get_executable_path();}Error OS_JavaScript::shell_open(String p_uri) {	// Open URI in a new tab, browser will deal with it by protocol.	/* clang-format off */	EM_ASM({		window.open(UTF8ToString($0), '_blank');	}, p_uri.utf8().get_data());	/* clang-format on */	return OK;}String OS_JavaScript::get_name() const {	return "HTML5";}bool OS_JavaScript::can_draw() const {	return true; // Always?}String OS_JavaScript::get_user_data_dir() const {	return "/userfs";};String OS_JavaScript::get_cache_path() const {	return "/home/web_user/.cache";}String OS_JavaScript::get_config_path() const {	return "/home/web_user/.config";}String OS_JavaScript::get_data_path() const {	return "/home/web_user/.local/share";}OS::PowerState OS_JavaScript::get_power_state() {	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");	return OS::POWERSTATE_UNKNOWN;}int OS_JavaScript::get_power_seconds_left() {	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");	return -1;}int OS_JavaScript::get_power_percent_left() {	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");	return -1;}void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) {	OS_JavaScript *os = get_singleton();	if (os->is_userfs_persistent() && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) {		os->last_sync_check_time = OS::get_singleton()->get_ticks_msec();		// Wait five seconds in case more files are about to be closed.		os->sync_wait_time = 5000;	}}void OS_JavaScript::set_idb_available(bool p_idb_available) {	idb_available = p_idb_available;}bool OS_JavaScript::is_userfs_persistent() const {	return idb_available;}OS_JavaScript *OS_JavaScript::get_singleton() {	return static_cast<OS_JavaScript *>(OS::get_singleton());}OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) {	List<String> arguments;	for (int i = 1; i < p_argc; i++) {		arguments.push_back(String::utf8(p_argv[i]));	}	set_cmdline(p_argv[0], arguments);	last_click_button_index = -1;	last_click_ms = 0;	last_click_pos = Point2(-100, -100);	last_width = 0;	last_height = 0;	window_maximized = false;	entering_fullscreen = false;	just_exited_fullscreen = false;	transparency_enabled = false;	main_loop = NULL;	visual_server = NULL;	audio_driver_javascript = NULL;	swap_ok_cancel = false;	idb_available = false;	sync_wait_time = -1;	if (AudioDriverJavaScript::is_available()) {		audio_driver_javascript = memnew(AudioDriverJavaScript);		AudioDriverManager::add_driver(audio_driver_javascript);	}	Vector<Logger *> loggers;	loggers.push_back(memnew(StdLogger));	_set_logger(memnew(CompositeLogger(loggers)));	FileAccessUnix::close_notification_func = file_access_close_callback;}
 |