| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157 | /*************************************************************************//*  os_javascript.cpp                                                    *//*************************************************************************//*                       This file is part of:                           *//*                           GODOT ENGINE                                *//*                      https://godotengine.org                          *//*************************************************************************//* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 *//* Copyright (c) 2014-2021 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/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 <dlfcn.h>#include <emscripten.h>#include <png.h>#include <stdlib.h>#include "dom_keys.inc"#include "godot_js.h"#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// Quitvoid OS_JavaScript::request_quit_callback() {	OS_JavaScript *os = get_singleton();	if (os && os->get_main_loop()) {		os->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_QUIT_REQUEST);	}}// Files drop (implemented in JS for now).void OS_JavaScript::drop_files_callback(char **p_filev, int p_filec) {	OS_JavaScript *os = get_singleton();	if (!os || !os->get_main_loop()) {		return;	}	Vector<String> files;	for (int i = 0; i < p_filec; i++) {		files.push_back(String::utf8(p_filev[i]));	}	os->get_main_loop()->drop_files(files);}void OS_JavaScript::send_notification_callback(int p_notification) {	OS_JavaScript *os = get_singleton();	if (!os) {		return;	}	if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {		os->cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;	}	MainLoop *loop = os->get_main_loop();	if (loop) {		loop->notification(p_notification);	}}// Window (canvas)bool OS_JavaScript::check_size_force_redraw() {	return godot_js_display_size_update() != 0;}void OS_JavaScript::fullscreen_change_callback(int p_fullscreen) {	OS_JavaScript *os = get_singleton();	os->video_mode.fullscreen = p_fullscreen;	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;	}}void OS_JavaScript::window_blur_callback() {	get_singleton()->input->release_pressed_events();}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 {	int size[2];	godot_js_display_screen_size_get(size, size + 1);	return Size2(size[0], size[1]);}void OS_JavaScript::set_window_size(const Size2 p_size) {	if (video_mode.fullscreen) {		window_maximized = false;		set_window_fullscreen(false);	} else {		if (window_maximized) {			emscripten_exit_soft_fullscreen();			window_maximized = false;		}		godot_js_display_desired_size_set(p_size.x, p_size.y);	}}Size2 OS_JavaScript::get_window_size() const {	int size[2];	godot_js_display_window_size_get(size, size + 1);	return Size2(size[0], size[1]);}void OS_JavaScript::set_window_maximized(bool p_enabled) {#ifndef TOOLS_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, &strategy);		window_maximized = p_enabled;	}#endif}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();		}		int result = godot_js_display_fullscreen_request();		ERR_FAIL_COND_MSG(result, "The request was denied. Remember that enabling fullscreen is only possible from an input callback for the HTML5 platform.");		entering_fullscreen = true;	} else {		// No logic allowed here, since exiting w/ ESC key won't use this function.		ERR_FAIL_COND(godot_js_display_fullscreen_exit());	}}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;}// Keysstatic void dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod) {	ev->set_shift(p_mod & 1);	ev->set_alt(p_mod & 2);	ev->set_control(p_mod & 4);	ev->set_metakey(p_mod & 8);}void OS_JavaScript::key_callback(int p_pressed, int p_repeat, int p_modifiers) {	OS_JavaScript *os = get_singleton();	JSKeyEvent &key_event = os->key_event;	// Resume audio context after input in case autoplay was denied.	os->resume_audio();	Ref<InputEventKey> ev;	ev.instance();	ev->set_echo(p_repeat);	ev->set_scancode(dom_code2godot_scancode(key_event.code, key_event.key, false));	ev->set_physical_scancode(dom_code2godot_scancode(key_event.code, key_event.key, true));	ev->set_pressed(p_pressed);	dom2godot_mod(ev, p_modifiers);	String unicode = String::utf8(key_event.key);	if (unicode.length() == 1) {		ev->set_unicode(unicode[0]);	}	os->input->parse_input_event(ev);	// Make sure to flush all events so we can call restricted APIs inside the event.	os->input->flush_buffered_events();}// 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();}int OS_JavaScript::mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers) {	OS_JavaScript *os = get_singleton();	Ref<InputEventMouseButton> ev;	ev.instance();	ev->set_pressed(p_pressed);	ev->set_position(Point2(p_x, p_y));	ev->set_global_position(ev->get_position());	ev->set_pressed(p_pressed);	dom2godot_mod(ev, p_modifiers);	switch (p_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 (p_pressed) {		uint64_t diff = (OS::get_singleton()->get_ticks_usec() / 1000) - 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()) {		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();	// Make sure to flush all events so we can call restricted APIs inside the event.	os->input->flush_buffered_events();	// Prevent multi-click text selection and wheel-click scrolling anchor.	// Context menu is prevented through contextmenu event.	return true;}void OS_JavaScript::mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers) {	OS_JavaScript *os = get_singleton();	int input_mask = os->input->get_mouse_button_mask();	// For motion outside the canvas, only read mouse movement if dragging	// started inside the canvas; imitating desktop app behaviour.	if (!os->cursor_inside_canvas && !input_mask)		return;	Ref<InputEventMouseMotion> ev;	ev.instance();	dom2godot_mod(ev, p_modifiers);	ev->set_button_mask(input_mask);	ev->set_position(Point2(p_x, p_y));	ev->set_global_position(ev->get_position());	ev->set_relative(Vector2(p_rel_x, p_rel_y));	os->input->set_mouse_position(ev->get_position());	ev->set_speed(os->input->get_last_mouse_speed());	os->input->parse_input_event(ev);}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";	}}void OS_JavaScript::set_cursor_shape(CursorShape p_shape) {	ERR_FAIL_INDEX(p_shape, CURSOR_MAX);	if (cursor_shape == p_shape) {		return;	}	cursor_shape = p_shape;	godot_js_display_cursor_set_shape(godot2dom_cursor(cursor_shape));}void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {	if (p_cursor.is_valid()) {		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();		godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), r.ptr(), len, p_hotspot.x, p_hotspot.y);		r = PoolByteArray::Read();	} else {		godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), NULL, 0, 0, 0);	}}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) {		godot_js_display_cursor_set_visible(1);		godot_js_display_cursor_lock_set(false);	} else if (p_mode == MOUSE_MODE_HIDDEN) {		godot_js_display_cursor_set_visible(0);		godot_js_display_cursor_lock_set(false);	} else if (p_mode == MOUSE_MODE_CAPTURED) {		godot_js_display_cursor_set_visible(1);		godot_js_display_cursor_lock_set(true);	}}OS::MouseMode OS_JavaScript::get_mouse_mode() const {	if (godot_js_display_cursor_is_hidden()) {		return MOUSE_MODE_HIDDEN;	}	if (godot_js_display_cursor_is_locked()) {		return MOUSE_MODE_CAPTURED;	}	return MOUSE_MODE_VISIBLE;}// Wheelint OS_JavaScript::mouse_wheel_callback(double p_delta_x, double p_delta_y) {	OS_JavaScript *os = get_singleton();	if (!godot_js_display_canvas_is_focused()) {		if (os->cursor_inside_canvas) {			godot_js_display_canvas_focus();		} else {			return false;		}	}	InputDefault *input = os->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_delta_y < 0) {		ev->set_button_index(BUTTON_WHEEL_UP);	} else if (p_delta_y > 0) {		ev->set_button_index(BUTTON_WHEEL_DOWN);	} else if (p_delta_x > 0) {		ev->set_button_index(BUTTON_WHEEL_LEFT);	} else if (p_delta_x < 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);	Ref<InputEventMouseButton> release = ev->duplicate();	release->set_pressed(false);	release->set_button_mask(input->get_mouse_button_mask() & ~button_flag);	input->parse_input_event(release);	return true;}// Touchbool OS_JavaScript::has_touchscreen_ui_hint() const {	return godot_js_display_touchscreen_is_available();}void OS_JavaScript::touch_callback(int p_type, int p_count) {	OS_JavaScript *os = get_singleton();	// Resume audio context after input in case autoplay was denied.	os->resume_audio();	const JSTouchEvent &touch_event = os->touch_event;	for (int i = 0; i < p_count; i++) {		Point2 point(touch_event.coords[i * 2], touch_event.coords[i * 2 + 1]);		if (p_type == 2) {			// touchmove			Ref<InputEventScreenDrag> ev;			ev.instance();			ev->set_index(touch_event.identifier[i]);			ev->set_position(point);			Point2 &prev = os->touches[i];			ev->set_relative(ev->get_position() - prev);			prev = ev->get_position();			os->input->parse_input_event(ev);		} else {			// touchstart/touchend			Ref<InputEventScreenTouch> ev;			ev.instance();			ev->set_index(touch_event.identifier[i]);			ev->set_position(point);			ev->set_pressed(p_type == 0);			os->touches[i] = point;			os->input->parse_input_event(ev);			// Make sure to flush all events so we can call restricted APIs inside the event.			os->input->flush_buffered_events();		}	}}// Gamepadvoid OS_JavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) {	InputDefault *input = get_singleton()->input;	if (p_connected) {		input->joy_connection_changed(p_index, true, String::utf8(p_id), String::utf8(p_guid));	} else {		input->joy_connection_changed(p_index, false, "");	}}void OS_JavaScript::process_joypads() {	int32_t pads = godot_js_input_gamepad_sample_count();	int32_t s_btns_num = 0;	int32_t s_axes_num = 0;	int32_t s_standard = 0;	float s_btns[16];	float s_axes[10];	for (int idx = 0; idx < pads; idx++) {		int err = godot_js_input_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard);		if (err) {			continue;		}		for (int b = 0; b < s_btns_num; b++) {			float value = s_btns[b];			// Buttons 6 and 7 in the standard mapping need to be			// axis to be handled as JOY_ANALOG by Godot.			if (s_standard && (b == 6 || b == 7)) {				InputDefault::JoyAxis joy_axis;				joy_axis.min = 0;				joy_axis.value = value;				int a = b == 6 ? JOY_ANALOG_L2 : JOY_ANALOG_R2;				input->joy_axis(idx, a, joy_axis);			} else {				input->joy_button(idx, b, value);			}		}		for (int a = 0; a < s_axes_num; a++) {			InputDefault::JoyAxis joy_axis;			joy_axis.min = -1;			joy_axis.value = s_axes[a];			input->joy_axis(idx, a, 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 audio_drivers.size();}const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {	if (audio_drivers.size() <= p_driver) {		return "Unknown";	}	return audio_drivers[p_driver]->get_name();}// Clipboardvoid OS_JavaScript::update_clipboard_callback(const char *p_text) {	// Only call set_clipboard from OS (sets local clipboard)	get_singleton()->OS::set_clipboard(p_text);}void OS_JavaScript::set_clipboard(const String &p_text) {	OS::set_clipboard(p_text);	int err = godot_js_display_clipboard_set(p_text.utf8().get_data());	ERR_FAIL_COND_MSG(err, "Clipboard API is not supported.");}String OS_JavaScript::get_clipboard() const {	godot_js_display_clipboard_get(update_clipboard_callback);	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();}Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {	video_mode = p_desired;	// fullscreen_change_callback will correct this if the request is successful.	video_mode.fullscreen = false;	// Handle contextmenu, webglcontextlost, initial canvas setup.	godot_js_display_setup_canvas(video_mode.width, video_mode.height, video_mode.fullscreen, is_hidpi_allowed() ? 1 : 0);	swap_ok_cancel = godot_js_display_is_swap_ok_cancel() == 1;	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);	}#ifdef TOOLS_ENABLED	bool gles3 = false;#else	bool gles3 = true;	if (p_video_driver == VIDEO_DRIVER_GLES2) {		gles3 = false;	}#endif	bool gl_initialization_error = false;	while (true) {		if (gles3) {			if (godot_js_display_has_webgl(2) && 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 (godot_js_display_has_webgl(1) && 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, &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;	AudioDriverManager::initialize(p_audio_driver);	visual_server = memnew(VisualServerRaster());#ifndef NO_THREADS	visual_server = memnew(VisualServerWrapMT(visual_server, false));#endif	input = memnew(InputDefault);	// JS Input interface (js/libs/library_godot_input.js)	godot_js_input_mouse_button_cb(&OS_JavaScript::mouse_button_callback);	godot_js_input_mouse_move_cb(&OS_JavaScript::mouse_move_callback);	godot_js_input_mouse_wheel_cb(&OS_JavaScript::mouse_wheel_callback);	godot_js_input_touch_cb(&OS_JavaScript::touch_callback, touch_event.identifier, touch_event.coords);	godot_js_input_key_cb(&OS_JavaScript::key_callback, key_event.code, key_event.key);	godot_js_input_gamepad_cb(&OS_JavaScript::gamepad_callback);	godot_js_input_paste_cb(&OS_JavaScript::update_clipboard_callback);	godot_js_input_drop_files_cb(&OS_JavaScript::drop_files_callback);	// JS Display interface (js/libs/library_godot_display.js)	godot_js_display_fullscreen_cb(&OS_JavaScript::fullscreen_change_callback);	godot_js_display_window_blur_cb(&window_blur_callback);	godot_js_display_notification_cb(&OS_JavaScript::send_notification_callback,			MainLoop::NOTIFICATION_WM_MOUSE_ENTER,			MainLoop::NOTIFICATION_WM_MOUSE_EXIT,			MainLoop::NOTIFICATION_WM_FOCUS_IN,			MainLoop::NOTIFICATION_WM_FOCUS_OUT);	godot_js_display_vk_cb(&input_text_callback);	visual_server->init();	return OK;}void OS_JavaScript::input_text_callback(const char *p_text, int p_cursor) {	OS_JavaScript *os = OS_JavaScript::get_singleton();	if (!os || !os->get_main_loop()) {		return;	}	os->get_main_loop()->input_text(String::utf8(p_text));	Ref<InputEventKey> k;	for (int i = 0; i < p_cursor; i++) {		k.instance();		k->set_pressed(true);		k->set_echo(false);		k->set_scancode(KEY_RIGHT);		os->input->parse_input_event(k);		k.instance();		k->set_pressed(false);		k->set_echo(false);		k->set_scancode(KEY_RIGHT);		os->input->parse_input_event(k);	}}bool OS_JavaScript::has_virtual_keyboard() const {	return godot_js_display_vk_available() != 0;}void OS_JavaScript::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {	godot_js_display_vk_show(p_existing_text.utf8().get_data(), p_multiline, p_cursor_start, p_cursor_end);}void OS_JavaScript::hide_virtual_keyboard() {	godot_js_display_vk_hide();}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() {	AudioDriverJavaScript::resume();}void OS_JavaScript::fs_sync_callback() {	get_singleton()->idb_is_syncing = false;}bool OS_JavaScript::main_loop_iterate() {	if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) {		idb_is_syncing = true;		idb_needs_sync = false;		godot_js_os_fs_sync(&OS_JavaScript::fs_sync_callback);	}	input->flush_buffered_events();	if (godot_js_input_gamepad_sample() == OK) {		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, &strategy);		} else {			godot_js_display_size_update();		}		just_exited_fullscreen = false;	}	return Main::iteration();}int OS_JavaScript::get_screen_dpi(int p_screen) const {	return godot_js_display_screen_dpi_get();}float OS_JavaScript::get_screen_scale(int p_screen) const {	return godot_js_display_pixel_ratio_get();}float OS_JavaScript::get_screen_max_scale() const {	return get_screen_scale();}void OS_JavaScript::delete_main_loop() {	memdelete(main_loop);	main_loop = NULL;}void OS_JavaScript::finalize() {	memdelete(input);	visual_server->finish();	emscripten_webgl_commit_frame();	memdelete(visual_server);	emscripten_webgl_destroy_context(webgl_ctx);	for (int i = 0; i < audio_drivers.size(); i++) {		memdelete(audio_drivers[i]);	}	audio_drivers.clear();}// 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);	int failed = godot_js_os_execute(json_args.utf8().get_data());	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.");}int OS_JavaScript::get_processor_count() const {	return godot_js_os_hw_concurrency_get();}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#ifndef NO_THREADS	if (p_feature == "threads") {		return true;	}#endif#if WASM_GDNATIVE	if (p_feature == "wasm32") {		return true;	}#endif	return false;}void OS_JavaScript::alert(const String &p_alert, const String &p_title) {	godot_js_display_alert(p_alert.utf8().get_data());}void OS_JavaScript::set_window_title(const String &p_title) {	godot_js_display_window_title_set(p_title.utf8().get_data());}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();	godot_js_display_window_icon_set(r.ptr(), len);}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.	godot_js_os_shell_open(p_uri.utf8().get_data());	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_flags & FileAccess::WRITE)) {		return; // FS persistence is not working or we are not writing.	}	bool is_file_persistent = p_file.begins_with("/userfs");#ifdef TOOLS_ENABLED	// Hack for editor persistence (can we track).	is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/");#endif	if (is_file_persistent) {		os->idb_needs_sync = true;	}}bool OS_JavaScript::is_userfs_persistent() const {	return idb_available;}Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) {	String path = p_path.get_file();	p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);	ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror());	return OK;}OS_JavaScript *OS_JavaScript::get_singleton() {	return static_cast<OS_JavaScript *>(OS::get_singleton());}OS_JavaScript::OS_JavaScript() {	// Expose method for requesting quit.	godot_js_os_request_quit_cb(&request_quit_callback);	// Set canvas ID	godot_js_config_canvas_id_get(canvas_id, sizeof(canvas_id));	cursor_inside_canvas = true;	cursor_shape = OS::CURSOR_ARROW;	last_click_button_index = -1;	last_click_ms = 0;	last_click_pos = Point2(-100, -100);	window_maximized = false;	entering_fullscreen = false;	just_exited_fullscreen = false;	transparency_enabled = false;	main_loop = NULL;	visual_server = NULL;	swap_ok_cancel = false;	idb_available = godot_js_os_fs_is_persistent() != 0;	idb_needs_sync = false;	idb_is_syncing = false;	if (AudioDriverJavaScript::is_available()) {#ifdef NO_THREADS		audio_drivers.push_back(memnew(AudioDriverScriptProcessor));#endif		audio_drivers.push_back(memnew(AudioDriverWorklet));	}	for (int i = 0; i < audio_drivers.size(); i++) {		AudioDriverManager::add_driver(audio_drivers[i]);	}	Vector<Logger *> loggers;	loggers.push_back(memnew(StdLogger));	_set_logger(memnew(CompositeLogger(loggers)));	FileAccessUnix::close_notification_func = file_access_close_callback;}
 |