diff options
Diffstat (limited to 'platform/javascript')
21 files changed, 2227 insertions, 1794 deletions
diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 7239648937..dcf9a46bf9 100644 --- a/platform/javascript/SCsub +++ b/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", @@ -17,22 +18,22 @@ if env["threads_enabled"]: build = env.add_program(build_targets, javascript_files) js_libraries = [ - "http_request.js", + "native/http_request.js", ] for lib in js_libraries: env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) env.Depends(build, js_libraries) -js_modules = [ - "id_handler.js", +js_pre = [ + "native/id_handler.js", + "native/utils.js", ] -for module in js_modules: - env.Append(LINKFLAGS=["--pre-js", env.File(module).path]) -env.Depends(build, js_modules) +for js in js_pre: + env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) +env.Depends(build, js_pre) engine = [ "engine/preloader.js", - "engine/loader.js", "engine/utils.js", "engine/engine.js", ] @@ -47,11 +48,17 @@ 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( diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 45cb82b351..9c73e5c4c4 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -35,26 +35,22 @@ static JavaScript *javascript_eval; void register_javascript_api() { - ClassDB::register_virtual_class<JavaScript>(); javascript_eval = memnew(JavaScript); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval)); } void unregister_javascript_api() { - memdelete(javascript_eval); } JavaScript *JavaScript::singleton = nullptr; JavaScript *JavaScript::get_singleton() { - return singleton; } JavaScript::JavaScript() { - ERR_FAIL_COND_MSG(singleton != nullptr, "JavaScript singleton already exist."); singleton = this; } @@ -62,13 +58,11 @@ JavaScript::JavaScript() { JavaScript::~JavaScript() {} void JavaScript::_bind_methods() { - ClassDB::bind_method(D_METHOD("eval", "code", "use_global_execution_context"), &JavaScript::eval, DEFVAL(false)); } #if !defined(JAVASCRIPT_ENABLED) || !defined(JAVASCRIPT_EVAL_ENABLED) Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - return Variant(); } #endif diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 8f857478e5..9604914b2c 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -30,27 +30,34 @@ #include "audio_driver_javascript.h" +#include "core/project_settings.h" + #include <emscripten.h> AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; -const char *AudioDriverJavaScript::get_name() const { +bool AudioDriverJavaScript::is_available() { + return EM_ASM_INT({ + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; + }) != 0; +} +const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() { - AudioDriverJavaScript::singleton->mix_to_js(); } extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { - AudioDriverJavaScript::singleton->process_capture(sample); } void AudioDriverJavaScript::mix_to_js() { - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); int sample_count = memarr_len(internal_buffer) / channel_count; int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); @@ -61,45 +68,41 @@ void AudioDriverJavaScript::mix_to_js() { } void AudioDriverJavaScript::process_capture(float sample) { - int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16); input_buffer_write(sample32); } Error AudioDriverJavaScript::init() { + int mix_rate = GLOBAL_GET("audio/mix_rate"); + int latency = GLOBAL_GET("audio/output_latency"); /* clang-format off */ _driver_id = EM_ASM_INT({ + const MIX_RATE = $0; + const LATENCY = $1 / 1000; return Module.IDHandler.add({ - 'context': new (window.AudioContext || window.webkitAudioContext), + 'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}), 'input': null, 'stream': null, 'script': null }); - }); + }, mix_rate, latency); /* clang-format on */ int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); + buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count); /* clang-format off */ buffer_length = EM_ASM_INT({ var ref = Module.IDHandler.get($0); - var ctx = ref['context']; - var CHANNEL_COUNT = $1; - - var channelCount = ctx.destination.channelCount; - var script = null; - try { - // Try letting the browser recommend a buffer length. - script = ctx.createScriptProcessor(0, 2, channelCount); - } catch (e) { - // ...otherwise, default to 4096. - script = ctx.createScriptProcessor(4096, 2, channelCount); - } + const ctx = ref['context']; + const BUFFER_LENGTH = $1; + const CHANNEL_COUNT = $2; + + var script = ctx.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT); script.connect(ctx.destination); ref['script'] = script; - return script.bufferSize; - }, _driver_id, channel_count); + }, _driver_id, buffer_length, channel_count); /* clang-format on */ if (!buffer_length) { return FAILED; @@ -115,7 +118,6 @@ Error AudioDriverJavaScript::init() { } void AudioDriverJavaScript::start() { - /* clang-format off */ EM_ASM({ const ref = Module.IDHandler.get($0); @@ -163,8 +165,26 @@ void AudioDriverJavaScript::resume() { /* clang-format on */ } -int AudioDriverJavaScript::get_mix_rate() const { +float AudioDriverJavaScript::get_latency() { + /* clang-format off */ + return EM_ASM_DOUBLE({ + const ref = Module.IDHandler.get($0); + var latency = 0; + if (ref && ref['context']) { + const ctx = ref['context']; + if (ctx.baseLatency) { + latency += ctx.baseLatency; + } + if (ctx.outputLatency) { + latency += ctx.outputLatency; + } + } + return latency; + }, _driver_id); + /* clang-format on */ +} +int AudioDriverJavaScript::get_mix_rate() const { /* clang-format off */ return EM_ASM_INT({ const ref = Module.IDHandler.get($0); @@ -174,7 +194,6 @@ int AudioDriverJavaScript::get_mix_rate() const { } AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { - /* clang-format off */ return get_speaker_mode_by_total_channels(EM_ASM_INT({ const ref = Module.IDHandler.get($0); @@ -190,23 +209,46 @@ 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({ - Module.IDHandler.remove($0); - }, _driver_id); + const id = $0; + var ref = Module.IDHandler.get(id); + Module.async_finish.push(new Promise(function(accept, reject) { + if (!ref) { + console.log("Ref not found!", id, Module.IDHandler); + setTimeout(accept, 0); + } else { + Module.IDHandler.remove(id); + 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(); + }); + } + })); + }, id); /* clang-format on */ +} +void AudioDriverJavaScript::finish() { if (internal_buffer) { memdelete_arr(internal_buffer); internal_buffer = nullptr; } - _driver_id = 0; } Error AudioDriverJavaScript::capture_start() { - input_buffer_init(buffer_length); /* clang-format off */ @@ -236,7 +278,6 @@ Error AudioDriverJavaScript::capture_start() { } Error AudioDriverJavaScript::capture_stop() { - /* clang-format off */ EM_ASM({ var ref = Module.IDHandler.get($0); @@ -262,10 +303,5 @@ Error AudioDriverJavaScript::capture_stop() { } AudioDriverJavaScript::AudioDriverJavaScript() { - - _driver_id = 0; - internal_buffer = nullptr; - buffer_length = 0; - singleton = this; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index f6f2dacd4e..f029a91db0 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -34,13 +34,13 @@ #include "servers/audio_server.h" class AudioDriverJavaScript : public AudioDriver { + float *internal_buffer = nullptr; - float *internal_buffer; - - int _driver_id; - int buffer_length; + int _driver_id = 0; + int buffer_length = 0; public: + static bool is_available(); void mix_to_js(); void process_capture(float sample); @@ -51,11 +51,13 @@ public: virtual Error init(); virtual void start(); void resume(); + virtual float get_latency(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; virtual void lock(); virtual void unlock(); virtual void finish(); + void finish_async(); virtual Error capture_start(); virtual Error capture_stop(); diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 9486e10717..81287cead8 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -22,7 +22,7 @@ def get_opts(): # 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 +57,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 +150,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,5 +162,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']"]) + # Add code that allow exiting runtime. + env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp new file mode 100644 index 0000000000..2f0a2faa83 --- /dev/null +++ b/platform/javascript/display_server_javascript.cpp @@ -0,0 +1,1186 @@ +/*************************************************************************/ +/* display_server_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 "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 + +char DisplayServerJavaScript::canvas_id[256] = { 0 }; +static bool cursor_inside_canvas = true; + +DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() { + return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton()); +} + +// Window (canvas) +void DisplayServerJavaScript::focus_canvas() { + /* clang-format off */ + EM_ASM( + Module['canvas'].focus(); + ); + /* clang-format on */ +} + +bool DisplayServerJavaScript::is_canvas_focused() { + /* clang-format off */ + return EM_ASM_INT_V( + return document.activeElement == Module['canvas']; + ); + /* clang-format on */ +} + +bool DisplayServerJavaScript::check_size_force_redraw() { + int canvas_width; + int canvas_height; + emscripten_get_canvas_element_size(DisplayServerJavaScript::canvas_id, &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(DisplayServerJavaScript::canvas_id, canvas_width, canvas_height); + return true; + } + return false; +} + +Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { + 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(canvas_id, &canvas_width, &canvas_height); + + double element_width; + double element_height; + emscripten_get_element_css_size(canvas_id, &element_width, &element_height); + + return Point2((int)(canvas_width / element_width * (p_x - canvas_x)), + (int)(canvas_height / element_height * (p_y - canvas_y))); +} + +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); + String canvas_str_id = String::utf8(canvas_id); + if (target_id.empty() || target_id == canvas_str_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; +} + +// Drag and drop callback (see native/utils.js). +extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { + DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); + if (!ds) { + ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); + } + if (ds->drop_files_callback.is_null()) + return; + Vector<String> files; + for (int i = 0; i < p_filec; i++) { + files.push_back(String::utf8(p_filev[i])); + } + Variant v = files; + Variant *vp = &v; + Variant ret; + Callable::CallError ce; + ds->drop_files_callback.call((const Variant **)&vp, 1, ret, ce); +} + +// Keys + +template <typename T> +void DisplayServerJavaScript::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); +} + +Ref<InputEventKey> DisplayServerJavaScript::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(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false)); + ev->set_physical_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, true)); + + 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 +const char *DisplayServerJavaScript::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"; + } +} + +void DisplayServerJavaScript::set_css_cursor(const char *p_cursor) { + /* clang-format off */ + EM_ASM_({ + Module['canvas'].style.cursor = UTF8ToString($0); + }, p_cursor); + /* clang-format on */ +} + +bool DisplayServerJavaScript::is_css_cursor_hidden() const { + /* 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]; + 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) { + 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, &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; +#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(canvas_id, mousedown, mouse_button_callback) + SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) + SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) + SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) + SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) + SET_EM_CALLBACK(canvas_id, touchend, touch_press_callback) + SET_EM_CALLBACK(canvas_id, touchcancel, touch_press_callback) + SET_EM_CALLBACK(canvas_id, keydown, keydown_callback) + SET_EM_CALLBACK(canvas_id, keypress, keypress_callback) + SET_EM_CALLBACK(canvas_id, 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 canvas = Module['canvas']; + 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]); + 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); + Module.listeners['dragover'] = function(ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }; + 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); + }, + 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, 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) { + drop_files_callback = p_callable; +} + +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) { + last_width = p_size.x; + last_height = p_size.y; + emscripten_set_canvas_element_size(canvas_id, 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, 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, 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(); +} diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h new file mode 100644 index 0000000000..b149665d67 --- /dev/null +++ b/platform/javascript/display_server_javascript.h @@ -0,0 +1,205 @@ +/*************************************************************************/ +/* 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; + + int last_width = 0; + int last_height = 0; + + // utilities + static Point2 compute_position_in_canvas(int p_x, int p_y); + static void focus_canvas(); + static bool is_canvas_focused(); + template <typename T> + static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event); + static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event); + static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape); + static void set_css_cursor(const char *p_cursor); + bool is_css_cursor_hidden() const; + + // events + 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(); + static char canvas_id[256]; + + WindowMode window_mode = WINDOW_MODE_WINDOWED; + + String clipboard; + + Callable window_event_callback; + Callable input_event_callback; + Callable input_text_callback; + Callable drop_files_callback; + + // utilities + bool check_size_force_redraw(); + + // 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 diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc index fd9df765d2..e3f2ce42b4 100644 --- a/platform/javascript/dom_keys.inc +++ b/platform/javascript/dom_keys.inc @@ -30,351 +30,203 @@ #include "core/os/keyboard.h" -// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Constants_for_keyCode_value -#define DOM_VK_CANCEL 0x03 -#define DOM_VK_HELP 0x06 -#define DOM_VK_BACK_SPACE 0x08 -#define DOM_VK_TAB 0x09 -#define DOM_VK_CLEAR 0x0C -#define DOM_VK_RETURN 0x0D -#define DOM_VK_ENTER 0x0E // "Reserved, but not used." -#define DOM_VK_SHIFT 0x10 -#define DOM_VK_CONTROL 0x11 -#define DOM_VK_ALT 0x12 -#define DOM_VK_PAUSE 0x13 -#define DOM_VK_CAPS_LOCK 0x14 -#define DOM_VK_KANA 0x15 -#define DOM_VK_HANGUL 0x15 -#define DOM_VK_EISU 0x16 -#define DOM_VK_JUNJA 0x17 -#define DOM_VK_FINAL 0x18 -#define DOM_VK_HANJA 0x19 -#define DOM_VK_KANJI 0x19 -#define DOM_VK_ESCAPE 0x1B -#define DOM_VK_CONVERT 0x1C -#define DOM_VK_NONCONVERT 0x1D -#define DOM_VK_ACCEPT 0x1E -#define DOM_VK_MODECHANGE 0x1F -#define DOM_VK_SPACE 0x20 -#define DOM_VK_PAGE_UP 0x21 -#define DOM_VK_PAGE_DOWN 0x22 -#define DOM_VK_END 0x23 -#define DOM_VK_HOME 0x24 -#define DOM_VK_LEFT 0x25 -#define DOM_VK_UP 0x26 -#define DOM_VK_RIGHT 0x27 -#define DOM_VK_DOWN 0x28 -#define DOM_VK_SELECT 0x29 -#define DOM_VK_PRINT 0x2A -#define DOM_VK_EXECUTE 0x2B -#define DOM_VK_PRINTSCREEN 0x2C -#define DOM_VK_INSERT 0x2D -#define DOM_VK_DELETE 0x2E -#define DOM_VK_0 0x30 -#define DOM_VK_1 0x31 -#define DOM_VK_2 0x32 -#define DOM_VK_3 0x33 -#define DOM_VK_4 0x34 -#define DOM_VK_5 0x35 -#define DOM_VK_6 0x36 -#define DOM_VK_7 0x37 -#define DOM_VK_8 0x38 -#define DOM_VK_9 0x39 -#define DOM_VK_COLON 0x3A -#define DOM_VK_SEMICOLON 0x3B -#define DOM_VK_LESS_THAN 0x3C -#define DOM_VK_EQUALS 0x3D -#define DOM_VK_GREATER_THAN 0x3E -#define DOM_VK_QUESTION_MARK 0x3F -#define DOM_VK_AT 0x40 -#define DOM_VK_A 0x41 -#define DOM_VK_B 0x42 -#define DOM_VK_C 0x43 -#define DOM_VK_D 0x44 -#define DOM_VK_E 0x45 -#define DOM_VK_F 0x46 -#define DOM_VK_G 0x47 -#define DOM_VK_H 0x48 -#define DOM_VK_I 0x49 -#define DOM_VK_J 0x4A -#define DOM_VK_K 0x4B -#define DOM_VK_L 0x4C -#define DOM_VK_M 0x4D -#define DOM_VK_N 0x4E -#define DOM_VK_O 0x4F -#define DOM_VK_P 0x50 -#define DOM_VK_Q 0x51 -#define DOM_VK_R 0x52 -#define DOM_VK_S 0x53 -#define DOM_VK_T 0x54 -#define DOM_VK_U 0x55 -#define DOM_VK_V 0x56 -#define DOM_VK_W 0x57 -#define DOM_VK_X 0x58 -#define DOM_VK_Y 0x59 -#define DOM_VK_Z 0x5A -#define DOM_VK_WIN 0x5B -#define DOM_VK_CONTEXT_MENU 0x5D -#define DOM_VK_SLEEP 0x5F -#define DOM_VK_NUMPAD0 0x60 -#define DOM_VK_NUMPAD1 0x61 -#define DOM_VK_NUMPAD2 0x62 -#define DOM_VK_NUMPAD3 0x63 -#define DOM_VK_NUMPAD4 0x64 -#define DOM_VK_NUMPAD5 0x65 -#define DOM_VK_NUMPAD6 0x66 -#define DOM_VK_NUMPAD7 0x67 -#define DOM_VK_NUMPAD8 0x68 -#define DOM_VK_NUMPAD9 0x69 -#define DOM_VK_MULTIPLY 0x6A -#define DOM_VK_ADD 0x6B -#define DOM_VK_SEPARATOR 0x6C -#define DOM_VK_SUBTRACT 0x6D -#define DOM_VK_DECIMAL 0x6E -#define DOM_VK_DIVIDE 0x6F -#define DOM_VK_F1 0x70 -#define DOM_VK_F2 0x71 -#define DOM_VK_F3 0x72 -#define DOM_VK_F4 0x73 -#define DOM_VK_F5 0x74 -#define DOM_VK_F6 0x75 -#define DOM_VK_F7 0x76 -#define DOM_VK_F8 0x77 -#define DOM_VK_F9 0x78 -#define DOM_VK_F10 0x79 -#define DOM_VK_F11 0x7A -#define DOM_VK_F12 0x7B -#define DOM_VK_F13 0x7C -#define DOM_VK_F14 0x7D -#define DOM_VK_F15 0x7E -#define DOM_VK_F16 0x7F -#define DOM_VK_F17 0x80 -#define DOM_VK_F18 0x81 -#define DOM_VK_F19 0x82 -#define DOM_VK_F20 0x83 -#define DOM_VK_F21 0x84 -#define DOM_VK_F22 0x85 -#define DOM_VK_F23 0x86 -#define DOM_VK_F24 0x87 -#define DOM_VK_NUM_LOCK 0x90 -#define DOM_VK_SCROLL_LOCK 0x91 -#define DOM_VK_WIN_OEM_FJ_JISHO 0x92 -#define DOM_VK_WIN_OEM_FJ_MASSHOU 0x93 -#define DOM_VK_WIN_OEM_FJ_TOUROKU 0x94 -#define DOM_VK_WIN_OEM_FJ_LOYA 0x95 -#define DOM_VK_WIN_OEM_FJ_ROYA 0x96 -#define DOM_VK_CIRCUMFLEX 0xA0 -#define DOM_VK_EXCLAMATION 0xA1 -#define DOM_VK_DOUBLE_QUOTE 0xA2 -#define DOM_VK_HASH 0xA3 -#define DOM_VK_DOLLAR 0xA4 -#define DOM_VK_PERCENT 0xA5 -#define DOM_VK_AMPERSAND 0xA6 -#define DOM_VK_UNDERSCORE 0xA7 -#define DOM_VK_OPEN_PAREN 0xA8 -#define DOM_VK_CLOSE_PAREN 0xA9 -#define DOM_VK_ASTERISK 0xAA -#define DOM_VK_PLUS 0xAB -#define DOM_VK_PIPE 0xAC -#define DOM_VK_HYPHEN_MINUS 0xAD -#define DOM_VK_OPEN_CURLY_BRACKET 0xAE -#define DOM_VK_CLOSE_CURLY_BRACKET 0xAF -#define DOM_VK_TILDE 0xB0 -#define DOM_VK_VOLUME_MUTE 0xB5 -#define DOM_VK_VOLUME_DOWN 0xB6 -#define DOM_VK_VOLUME_UP 0xB7 -#define DOM_VK_COMMA 0xBC -#define DOM_VK_PERIOD 0xBE -#define DOM_VK_SLASH 0xBF -#define DOM_VK_BACK_QUOTE 0xC0 -#define DOM_VK_OPEN_BRACKET 0xDB -#define DOM_VK_BACK_SLASH 0xDC -#define DOM_VK_CLOSE_BRACKET 0xDD -#define DOM_VK_QUOTE 0xDE -#define DOM_VK_META 0xE0 -#define DOM_VK_ALTGR 0xE1 -#define DOM_VK_WIN_ICO_HELP 0xE3 -#define DOM_VK_WIN_ICO_00 0xE4 -#define DOM_VK_WIN_ICO_CLEAR 0xE6 -#define DOM_VK_WIN_OEM_RESET 0xE9 -#define DOM_VK_WIN_OEM_JUMP 0xEA -#define DOM_VK_WIN_OEM_PA1 0xEB -#define DOM_VK_WIN_OEM_PA2 0xEC -#define DOM_VK_WIN_OEM_PA3 0xED -#define DOM_VK_WIN_OEM_WSCTRL 0xEE -#define DOM_VK_WIN_OEM_CUSEL 0xEF -#define DOM_VK_WIN_OEM_ATTN 0xF0 -#define DOM_VK_WIN_OEM_FINISH 0xF1 -#define DOM_VK_WIN_OEM_COPY 0xF2 -#define DOM_VK_WIN_OEM_AUTO 0xF3 -#define DOM_VK_WIN_OEM_ENLW 0xF4 -#define DOM_VK_WIN_OEM_BACKTAB 0xF5 -#define DOM_VK_ATTN 0xF6 -#define DOM_VK_CRSEL 0xF7 -#define DOM_VK_EXSEL 0xF8 -#define DOM_VK_EREOF 0xF9 -#define DOM_VK_PLAY 0xFA -#define DOM_VK_ZOOM 0xFB -#define DOM_VK_PA1 0xFD -#define DOM_VK_WIN_OEM_CLEAR 0xFE - -int dom2godot_keycode(int dom_keycode) { - - if (DOM_VK_0 <= dom_keycode && dom_keycode <= DOM_VK_Z) { - // ASCII intersection - return dom_keycode; - } - - if (DOM_VK_NUMPAD0 <= dom_keycode && dom_keycode <= DOM_VK_NUMPAD9) { - // Numpad numbers - return KEY_KP_0 + (dom_keycode - DOM_VK_NUMPAD0); +// See https://w3c.github.io/uievents-code/#code-value-tables +int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) { +#define DOM2GODOT(p_str, p_godot_code) \ + if (memcmp((const void *)p_str, (void *)p_code, strlen(p_str) + 1) == 0) { \ + return KEY_##p_godot_code; \ } - if (DOM_VK_F1 <= dom_keycode && dom_keycode <= DOM_VK_F16) { - // F1-F16 - return KEY_F1 + (dom_keycode - DOM_VK_F1); + // Numpad section. + DOM2GODOT("NumLock", NUMLOCK); + DOM2GODOT("Numpad0", KP_0); + DOM2GODOT("Numpad1", KP_1); + DOM2GODOT("Numpad2", KP_2); + DOM2GODOT("Numpad3", KP_3); + DOM2GODOT("Numpad4", KP_4); + DOM2GODOT("Numpad5", KP_5); + DOM2GODOT("Numpad6", KP_6); + DOM2GODOT("Numpad7", KP_7); + DOM2GODOT("Numpad8", KP_8); + DOM2GODOT("Numpad9", KP_9); + DOM2GODOT("NumpadAdd", KP_ADD); + DOM2GODOT("NumpadBackspace", BACKSPACE); + DOM2GODOT("NumpadClear", CLEAR); + DOM2GODOT("NumpadClearEntry", CLEAR); + //DOM2GODOT("NumpadComma", UNKNOWN); + DOM2GODOT("NumpadDecimal", KP_PERIOD); + DOM2GODOT("NumpadDivide", KP_DIVIDE); + DOM2GODOT("NumpadEnter", KP_ENTER); + DOM2GODOT("NumpadEqual", EQUAL); + //DOM2GODOT("NumpadHash", UNKNOWN); + //DOM2GODOT("NumpadMemoryAdd", UNKNOWN); + //DOM2GODOT("NumpadMemoryClear", UNKNOWN); + //DOM2GODOT("NumpadMemoryRecall", UNKNOWN); + //DOM2GODOT("NumpadMemoryStore", UNKNOWN); + //DOM2GODOT("NumpadMemorySubtract", UNKNOWN); + DOM2GODOT("NumpadMultiply", KP_MULTIPLY); + DOM2GODOT("NumpadParenLeft", PARENLEFT); + DOM2GODOT("NumpadParenRight", PARENRIGHT); + DOM2GODOT("NumpadStar", KP_MULTIPLY); // or ASTERISK ? + DOM2GODOT("NumpadSubtract", KP_SUBTRACT); + + // Printable ASCII. + if (!p_physical) { + uint8_t b0 = (uint8_t)p_key[0]; + uint8_t b1 = (uint8_t)p_key[1]; + uint8_t b2 = (uint8_t)p_key[2]; + if (b1 == 0 && b0 > 0x1F && b0 < 0x7F) { // ASCII. + if (b0 > 0x60 && b0 < 0x7B) { // Lowercase ASCII. + b0 -= 32; + } + return b0; + } + +#define _U_2BYTES_MASK 0xE0 +#define _U_2BYTES 0xC0 + // Latin-1 codes. + if (b2 == 0 && (b0 & _U_2BYTES_MASK) == _U_2BYTES) { // 2-bytes utf8, only known latin. + uint32_t key = ((b0 & ~_U_2BYTES_MASK) << 6) | (b1 & 0x3F); + if (key >= 0xA0 && key <= 0xDF) { + return key; + } + if (key >= 0xE0 && key <= 0xFF) { // Lowercase known latin. + key -= 0x20; + return key; + } + } +#undef _U_2BYTES_MASK +#undef _U_2BYTES } - switch (dom_keycode) { - //case DOM_VK_CANCEL: return KEY_UNKNOWN; - case DOM_VK_HELP: return KEY_HELP; - case DOM_VK_BACK_SPACE: return KEY_BACKSPACE; - case DOM_VK_TAB: return KEY_TAB; - - case DOM_VK_CLEAR: - case DOM_VK_WIN_OEM_CLEAR: // OEM duplicate - return KEY_CLEAR; - - case DOM_VK_RETURN: - case DOM_VK_ENTER: // unused according to MDN - return KEY_ENTER; - - case DOM_VK_SHIFT: return KEY_SHIFT; - case DOM_VK_CONTROL: return KEY_CONTROL; - - case DOM_VK_ALT: - case DOM_VK_ALTGR: - return KEY_ALT; - - case DOM_VK_PAUSE: return KEY_PAUSE; - case DOM_VK_CAPS_LOCK: - return KEY_CAPSLOCK; - - /* - case DOM_VK_KANA: return KEY_UNKNOWN; - case DOM_VK_HANGUL: return KEY_UNKNOWN; - case DOM_VK_EISU: return KEY_UNKNOWN; - case DOM_VK_JUNJA: return KEY_UNKNOWN; - case DOM_VK_FINAL: return KEY_UNKNOWN; - case DOM_VK_HANJA: return KEY_UNKNOWN; - case DOM_VK_KANJI: return KEY_UNKNOWN; - */ - - case DOM_VK_ESCAPE: - return KEY_ESCAPE; - /* - case DOM_VK_CONVERT: return KEY_UNKNOWN; - case DOM_VK_NONCONVERT: return KEY_UNKNOWN; - case DOM_VK_ACCEPT: return KEY_UNKNOWN; - case DOM_VK_MODECHANGE: return KEY_UNKNOWN; - */ - - case DOM_VK_SPACE: return KEY_SPACE; - case DOM_VK_PAGE_UP: return KEY_PAGEUP; - case DOM_VK_PAGE_DOWN: return KEY_PAGEDOWN; - case DOM_VK_END: return KEY_END; - case DOM_VK_HOME: return KEY_HOME; - case DOM_VK_LEFT: return KEY_LEFT; - case DOM_VK_UP: return KEY_UP; - case DOM_VK_RIGHT: return KEY_RIGHT; - case DOM_VK_DOWN: - return KEY_DOWN; - - //case DOM_VK_SELECT: return KEY_UNKNOWN; - - case DOM_VK_PRINTSCREEN: - case DOM_VK_PRINT: - return KEY_PRINT; - - //case DOM_VK_EXECUTE: return KEY_UNKNOWN; - case DOM_VK_INSERT: return KEY_INSERT; - case DOM_VK_DELETE: return KEY_DELETE; - - case DOM_VK_META: - case DOM_VK_WIN: - return KEY_META; - - case DOM_VK_CONTEXT_MENU: return KEY_MENU; - case DOM_VK_SLEEP: - return KEY_STANDBY; - - // Numpad keys - case DOM_VK_MULTIPLY: return KEY_KP_MULTIPLY; - case DOM_VK_ADD: return KEY_KP_ADD; - case DOM_VK_SEPARATOR: - return KEY_KP_PERIOD; // Good enough? - case DOM_VK_SUBTRACT: return KEY_KP_SUBTRACT; - case DOM_VK_DECIMAL: return KEY_KP_PERIOD; - case DOM_VK_DIVIDE: - return KEY_KP_DIVIDE; - - /* - case DOM_VK_F17: return KEY_UNKNOWN; - case DOM_VK_F18: return KEY_UNKNOWN; - case DOM_VK_F19: return KEY_UNKNOWN; - case DOM_VK_F20: return KEY_UNKNOWN; - case DOM_VK_F21: return KEY_UNKNOWN; - case DOM_VK_F22: return KEY_UNKNOWN; - case DOM_VK_F23: return KEY_UNKNOWN; - case DOM_VK_F24: return KEY_UNKNOWN; - */ - - case DOM_VK_NUM_LOCK: return KEY_NUMLOCK; - case DOM_VK_SCROLL_LOCK: - return KEY_SCROLLLOCK; - - /* - case DOM_VK_WIN_OEM_FJ_JISHO: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_MASSHOU: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_TOUROKU: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_LOYA: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_ROYA: return KEY_UNKNOWN; - */ - - case DOM_VK_CIRCUMFLEX: return KEY_ASCIICIRCUM; - case DOM_VK_EXCLAMATION: return KEY_EXCLAM; - case DOM_VK_DOUBLE_QUOTE: return KEY_QUOTEDBL; - case DOM_VK_HASH: return KEY_NUMBERSIGN; - case DOM_VK_DOLLAR: return KEY_DOLLAR; - case DOM_VK_PERCENT: return KEY_PERCENT; - case DOM_VK_AMPERSAND: return KEY_AMPERSAND; - case DOM_VK_UNDERSCORE: return KEY_UNDERSCORE; - case DOM_VK_OPEN_PAREN: return KEY_PARENLEFT; - case DOM_VK_CLOSE_PAREN: return KEY_PARENRIGHT; - case DOM_VK_ASTERISK: return KEY_ASTERISK; - case DOM_VK_PLUS: return KEY_PLUS; - case DOM_VK_PIPE: return KEY_BAR; - case DOM_VK_HYPHEN_MINUS: return KEY_MINUS; - case DOM_VK_OPEN_CURLY_BRACKET: return KEY_BRACELEFT; - case DOM_VK_CLOSE_CURLY_BRACKET: return KEY_BRACERIGHT; - case DOM_VK_TILDE: return KEY_ASCIITILDE; - - case DOM_VK_VOLUME_MUTE: return KEY_VOLUMEMUTE; - case DOM_VK_VOLUME_DOWN: return KEY_VOLUMEDOWN; - case DOM_VK_VOLUME_UP: return KEY_VOLUMEUP; - - case DOM_VK_COMMA: return KEY_COMMA; - case DOM_VK_PERIOD: return KEY_PERIOD; - case DOM_VK_SLASH: return KEY_SLASH; - case DOM_VK_BACK_QUOTE: return KEY_QUOTELEFT; - case DOM_VK_OPEN_BRACKET: return KEY_BRACKETLEFT; - case DOM_VK_BACK_SLASH: return KEY_BACKSLASH; - case DOM_VK_CLOSE_BRACKET: return KEY_BRACKETRIGHT; - case DOM_VK_QUOTE: - return KEY_APOSTROPHE; - - // The rest is OEM/unusual. - - default: return KEY_UNKNOWN; - }; + // Alphanumeric section. + DOM2GODOT("Backquote", QUOTELEFT); + DOM2GODOT("Backslash", BACKSLASH); + DOM2GODOT("BracketLeft", BRACKETLEFT); + DOM2GODOT("BracketRight", BRACKETRIGHT); + DOM2GODOT("Comma", COMMA); + DOM2GODOT("Digit0", 0); + DOM2GODOT("Digit1", 1); + DOM2GODOT("Digit2", 2); + DOM2GODOT("Digit3", 3); + DOM2GODOT("Digit4", 4); + DOM2GODOT("Digit5", 5); + DOM2GODOT("Digit6", 6); + DOM2GODOT("Digit7", 7); + DOM2GODOT("Digit8", 8); + DOM2GODOT("Digit9", 9); + DOM2GODOT("Equal", EQUAL); + DOM2GODOT("IntlBackslash", BACKSLASH); + //DOM2GODOT("IntlRo", UNKNOWN); + DOM2GODOT("IntlYen", YEN); + + DOM2GODOT("KeyA", A); + DOM2GODOT("KeyB", B); + DOM2GODOT("KeyC", C); + DOM2GODOT("KeyD", D); + DOM2GODOT("KeyE", E); + DOM2GODOT("KeyF", F); + DOM2GODOT("KeyG", G); + DOM2GODOT("KeyH", H); + DOM2GODOT("KeyI", I); + DOM2GODOT("KeyJ", J); + DOM2GODOT("KeyK", K); + DOM2GODOT("KeyL", L); + DOM2GODOT("KeyM", M); + DOM2GODOT("KeyN", N); + DOM2GODOT("KeyO", O); + DOM2GODOT("KeyP", P); + DOM2GODOT("KeyQ", Q); + DOM2GODOT("KeyR", R); + DOM2GODOT("KeyS", S); + DOM2GODOT("KeyT", T); + DOM2GODOT("KeyU", U); + DOM2GODOT("KeyV", V); + DOM2GODOT("KeyW", W); + DOM2GODOT("KeyX", X); + DOM2GODOT("KeyY", Y); + DOM2GODOT("KeyZ", Z); + + DOM2GODOT("Minus", MINUS); + DOM2GODOT("Period", PERIOD); + DOM2GODOT("Quote", APOSTROPHE); + DOM2GODOT("Semicolon", SEMICOLON); + DOM2GODOT("Slash", SLASH); + + // Functional keys in the Alphanumeric section. + DOM2GODOT("AltLeft", ALT); + DOM2GODOT("AltRight", ALT); + DOM2GODOT("Backspace", BACKSPACE); + DOM2GODOT("CapsLock", CAPSLOCK); + DOM2GODOT("ContextMenu", MENU); + DOM2GODOT("ControlLeft", CONTROL); + DOM2GODOT("ControlRight", CONTROL); + DOM2GODOT("Enter", ENTER); + DOM2GODOT("MetaLeft", SUPER_L); + DOM2GODOT("MetaRight", SUPER_R); + DOM2GODOT("ShiftLeft", SHIFT); + DOM2GODOT("ShiftRight", SHIFT); + DOM2GODOT("Space", SPACE); + DOM2GODOT("Tab", TAB); + + // ControlPad section. + DOM2GODOT("Delete", DELETE); + DOM2GODOT("End", END); + DOM2GODOT("Help", HELP); + DOM2GODOT("Home", HOME); + DOM2GODOT("Insert", INSERT); + DOM2GODOT("PageDown", PAGEDOWN); + DOM2GODOT("PageUp", PAGEUP); + + // ArrowPad section. + DOM2GODOT("ArrowDown", DOWN); + DOM2GODOT("ArrowLeft", LEFT); + DOM2GODOT("ArrowRight", RIGHT); + DOM2GODOT("ArrowUp", UP); + + // Function section. + DOM2GODOT("Escape", ESCAPE); + DOM2GODOT("F1", F1); + DOM2GODOT("F2", F2); + DOM2GODOT("F3", F3); + DOM2GODOT("F4", F4); + DOM2GODOT("F5", F5); + DOM2GODOT("F6", F6); + DOM2GODOT("F7", F7); + DOM2GODOT("F8", F8); + DOM2GODOT("F9", F9); + DOM2GODOT("F10", F10); + DOM2GODOT("F11", F11); + DOM2GODOT("F12", F12); + //DOM2GODOT("Fn", UNKNOWN); // never actually fired, but included in the standard draft. + //DOM2GODOT("FnLock", UNKNOWN); + DOM2GODOT("PrintScreen", PRINT); + DOM2GODOT("ScrollLock", SCROLLLOCK); + DOM2GODOT("Pause", PAUSE); + + // Media keys section. + DOM2GODOT("BrowserBack", BACK); + DOM2GODOT("BrowserFavorites", FAVORITES); + DOM2GODOT("BrowserForward", FORWARD); + DOM2GODOT("BrowserHome", OPENURL); + DOM2GODOT("BrowserRefresh", REFRESH); + DOM2GODOT("BrowserSearch", SEARCH); + DOM2GODOT("BrowserStop", STOP); + //DOM2GODOT("Eject", UNKNOWN); + DOM2GODOT("LaunchApp1", LAUNCH0); + DOM2GODOT("LaunchApp2", LAUNCH1); + DOM2GODOT("LaunchMail", LAUNCHMAIL); + DOM2GODOT("MediaPlayPause", MEDIAPLAY); + DOM2GODOT("MediaSelect", LAUNCHMEDIA); + DOM2GODOT("MediaStop", MEDIASTOP); + DOM2GODOT("MediaTrackNext", MEDIANEXT); + DOM2GODOT("MediaTrackPrevious", MEDIAPREVIOUS); + //DOM2GODOT("Power", UNKNOWN); + //DOM2GODOT("Sleep", UNKNOWN); + DOM2GODOT("AudioVolumeDown", VOLUMEDOWN); + DOM2GODOT("AudioVolumeMute", VOLUMEMUTE); + DOM2GODOT("AudioVolumeUp", VOLUMEUP); + //DOM2GODOT("WakeUp", UNKNOWN); + return KEY_UNKNOWN; +#undef DOM2GODOT } diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index 6d7509377f..d709422abb 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -1,16 +1,8 @@ Function('return this')()['Engine'] = (function() { - - var unloadAfterInit = true; - var canvas = null; - var resizeCanvasOnStart = false; - var customLocale = 'en_US'; - var wasmExt = '.wasm'; - var preloader = new Preloader(); - var loader = new Loader(); - var rtenv = null; - var executableName = ''; + var wasmExt = '.wasm'; + var unloadAfterInit = true; var loadPath = ''; var loadPromise = null; var initPromise = null; @@ -33,31 +25,43 @@ Function('return this')()['Engine'] = (function() { }; /** @constructor */ - function Engine() {}; + function Engine() { + this.canvas = null; + this.executableName = ''; + this.rtenv = null; + this.customLocale = null; + this.resizeCanvasOnStart = false; + this.onExecute = null; + this.onExit = null; + }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { if (initPromise) { return initPromise; } - if (!loadPromise) { + if (loadPromise == null) { if (!basePath) { initPromise = Promise.reject(new Error("A base path must be provided when calling `init` and the engine is not loaded.")); return initPromise; } load(basePath); } - var config = {} + var config = {}; if (typeof stdout === 'function') config.print = stdout; if (typeof stderr === 'function') config.printErr = stderr; - initPromise = loader.init(loadPromise, loadPath, config).then(function() { - return new Promise(function(resolve, reject) { - rtenv = loader.env; + var me = this; + initPromise = new Promise(function(resolve, reject) { + config['locateFile'] = Utils.createLocateRewrite(loadPath); + config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); + Godot(config).then(function(module) { + me.rtenv = module; if (unloadAfterInit) { - loadPromise = null; + unload(); } resolve(); + config = null; }); }); return initPromise; @@ -76,33 +80,72 @@ Function('return this')()['Engine'] = (function() { args.push(arguments[i]); } var me = this; - return new Promise(function(resolve, reject) { - return me.init().then(function() { - if (!(canvas instanceof HTMLCanvasElement)) { - canvas = Utils.findCanvas(); - } - rtenv['locale'] = customLocale; - rtenv['canvas'] = canvas; - rtenv['thisProgram'] = executableName; - rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart; - loader.start(preloader.preloadedFiles, args).then(function() { - loader = null; - initPromise = null; - resolve(); + return me.init().then(function() { + if (!me.rtenv) { + return Promise.reject(new Error('The engine must be initialized before it can be started')); + } + + if (!(me.canvas instanceof HTMLCanvasElement)) { + me.canvas = Utils.findCanvas(); + } + + // Canvas can grab focus on click, or key events won't work. + if (me.canvas.tabIndex < 0) { + me.canvas.tabIndex = 0; + } + + // Disable right-click context menu. + me.canvas.addEventListener('contextmenu', function(ev) { + ev.preventDefault(); + }, false); + + // Until context restoration is implemented warn the user of context loss. + me.canvas.addEventListener('webglcontextlost', function(ev) { + alert("WebGL context lost, please reload the page"); + ev.preventDefault(); + }, false); + + // Browser locale, or custom one if defined. + var locale = me.customLocale; + if (!locale) { + locale = navigator.languages ? navigator.languages[0] : navigator.language; + locale = locale.split('.')[0]; + } + me.rtenv['locale'] = locale; + me.rtenv['canvas'] = me.canvas; + 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); + me.rtenv = null; + } + return new Promise(function(resolve, reject) { + preloader.preloadedFiles.forEach(function(file) { + me.rtenv['copyToFS'](file.path, file.buffer); }); + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](args); + initPromise = null; + resolve(); }); }); }; - Engine.prototype.startGame = function(execName, mainPack) { + Engine.prototype.startGame = function(execName, mainPack, extraArgs) { // Start and init with execName as loadPath if not inited. - executableName = execName; + this.executableName = execName; var me = this; return Promise.all([ this.init(execName), this.preloadFile(mainPack, mainPack) ]).then(function() { - return me.start('--main-pack', mainPack); + var args = ['--main-pack', mainPack]; + if (extraArgs) + args = args.concat(extraArgs); + return me.start.apply(me, args); }); }; @@ -118,67 +161,85 @@ Function('return this')()['Engine'] = (function() { }; Engine.prototype.setCanvas = function(canvasElem) { - canvas = canvasElem; + this.canvas = canvasElem; }; Engine.prototype.setCanvasResizedOnStart = function(enabled) { - resizeCanvasOnStart = enabled; + this.resizeCanvasOnStart = enabled; }; Engine.prototype.setLocale = function(locale) { - customLocale = locale; + this.customLocale = locale; }; Engine.prototype.setExecutableName = function(newName) { - executableName = newName; + this.executableName = newName; }; Engine.prototype.setProgressFunc = function(func) { progressFunc = func; - } + }; Engine.prototype.setStdoutFunc = function(func) { - var print = function(text) { if (arguments.length > 1) { text = Array.prototype.slice.call(arguments).join(" "); } func(text); }; - if (rtenv) - rtenv.print = print; + if (this.rtenv) + this.rtenv.print = print; stdout = print; }; Engine.prototype.setStderrFunc = function(func) { - var printErr = function(text) { if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(" "); func(text); }; - if (rtenv) - rtenv.printErr = printErr; + if (this.rtenv) + this.rtenv.printErr = printErr; 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; + } + + Engine.prototype.copyToFS = function(path, buffer) { + if (this.rtenv == null) { + throw new Error("Engine must be inited before copying files"); + } + this.rtenv['copyToFS'](path, buffer); + } + // Closure compiler exported engine methods. /** @export */ Engine['isWebGLAvailable'] = Utils.isWebGLAvailable; Engine['load'] = load; Engine['unload'] = unload; - Engine.prototype['init'] = Engine.prototype.init - Engine.prototype['preloadFile'] = Engine.prototype.preloadFile - Engine.prototype['start'] = Engine.prototype.start - Engine.prototype['startGame'] = Engine.prototype.startGame - Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension - Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit - Engine.prototype['setCanvas'] = Engine.prototype.setCanvas - Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart - Engine.prototype['setLocale'] = Engine.prototype.setLocale - Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName - Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc - Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc - Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc + Engine.prototype['init'] = Engine.prototype.init; + Engine.prototype['preloadFile'] = Engine.prototype.preloadFile; + Engine.prototype['start'] = Engine.prototype.start; + Engine.prototype['startGame'] = Engine.prototype.startGame; + Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension; + Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit; + Engine.prototype['setCanvas'] = Engine.prototype.setCanvas; + Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart; + Engine.prototype['setLocale'] = Engine.prototype.setLocale; + Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName; + 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; })(); diff --git a/platform/javascript/engine/loader.js b/platform/javascript/engine/loader.js deleted file mode 100644 index d27fbf612e..0000000000 --- a/platform/javascript/engine/loader.js +++ /dev/null @@ -1,33 +0,0 @@ -var Loader = /** @constructor */ function() { - - this.env = null; - - this.init = function(loadPromise, basePath, config) { - var me = this; - return new Promise(function(resolve, reject) { - var cfg = config || {}; - cfg['locateFile'] = Utils.createLocateRewrite(basePath); - cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); - loadPromise = null; - Godot(cfg).then(function(module) { - me.env = module; - resolve(); - }); - }); - } - - this.start = function(preloadedFiles, args) { - var me = this; - return new Promise(function(resolve, reject) { - if (!me.env) { - reject(new Error('The engine must be initialized before it can be started')); - } - preloadedFiles.forEach(function(file) { - Utils.copyToFS(me.env['FS'], file.path, file.buffer); - }); - preloadedFiles.length = 0; // Clear memory - me.env['callMain'](args); - resolve(); - }); - } -}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js index fdff90a923..0c97b38199 100644 --- a/platform/javascript/engine/utils.js +++ b/platform/javascript/engine/utils.js @@ -27,24 +27,6 @@ var Utils = { return instantiateWasm; }, - copyToFS: function(fs, path, buffer) { - var p = path.lastIndexOf("/"); - var dir = "/"; - if (p > 0) { - dir = path.slice(0, path.lastIndexOf("/")); - } - try { - fs.stat(dir); - } catch (e) { - if (e.errno !== 44) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h - throw e; - } - fs['mkdirTree'](dir); - } - // With memory growth, canOwn should be false. - fs['writeFile'](path, new Uint8Array(buffer), {'flags': 'wx+'}); - }, - findCanvas: function() { var nodes = document.getElementsByTagName('canvas'); if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index 36076a2af9..3573ddac95 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -28,6 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "core/io/json.h" #include "core/io/tcp_server.h" #include "core/io/zip_io.h" #include "editor/editor_export.h" @@ -40,7 +41,6 @@ #define EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG "webassembly_debug.zip" class EditorHTTPServer : public Reference { - private: Ref<TCP_Server> server; Ref<StreamPeerTCP> connection; @@ -148,11 +148,13 @@ public: } void poll() { - if (!server->is_listening()) + if (!server->is_listening()) { return; + } if (connection.is_null()) { - if (!server->is_connection_available()) + if (!server->is_connection_available()) { return; + } connection = server->take_connection(); time = OS::get_singleton()->get_ticks_usec(); } @@ -160,11 +162,11 @@ public: _clear_client(); return; } - if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED) + if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED) { return; + } while (true) { - char *r = (char *)req_buf; int l = req_pos - 1; if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { @@ -190,7 +192,6 @@ public: }; class EditorExportPlatformJavaScript : public EditorExportPlatform { - GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform); Ref<ImageTexture> logo; @@ -198,7 +199,7 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform { Ref<ImageTexture> stop_icon; int menu_options; - void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug); + void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags); private: Ref<EditorHTTPServer> server; @@ -230,7 +231,6 @@ public: virtual Ref<Texture2D> get_run_icon() const; virtual void get_platform_features(List<String> *r_features) { - r_features->push_back("web"); r_features->push_back(get_os_name()); } @@ -238,23 +238,28 @@ public: virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { } + String get_debug_protocol() const { return "ws://"; } + EditorExportPlatformJavaScript(); ~EditorExportPlatformJavaScript(); }; -void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug) { - +void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags) { String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size()); String str_export; Vector<String> lines = str_template.split("\n"); + Vector<String> flags; + String flags_json; + gen_export_flags(flags, p_flags); + flags_json = JSON::print(flags); for (int i = 0; i < lines.size(); i++) { - String current_line = lines[i]; current_line = current_line.replace("$GODOT_BASENAME", p_name); current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name")); current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include")); current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false"); + current_line = current_line.replace("$GODOT_ARGS", flags_json); str_export += current_line + "\n"; } @@ -266,7 +271,6 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re } void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { - if (p_preset->get("vram_texture_compression/for_desktop")) { r_features->push_back("s3tc"); } @@ -283,7 +287,6 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP } void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_options) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), "")); @@ -293,22 +296,18 @@ void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_op } String EditorExportPlatformJavaScript::get_name() const { - return "HTML5"; } String EditorExportPlatformJavaScript::get_os_name() const { - return "HTML5"; } Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const { - return logo; } bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { - String err; bool valid = false; @@ -343,14 +342,14 @@ bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p } } - if (!err.empty()) + if (!err.empty()) { r_error = err; + } return valid; } List<String> EditorExportPlatformJavaScript::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { - List<String> list; list.push_back("html"); return list; @@ -368,11 +367,11 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese template_path = template_path.strip_edges(); if (template_path == String()) { - - if (p_debug) + if (p_debug) { template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG); - else + } else { template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE); + } } if (!DirAccess::exists(p_path.get_base_dir())) { @@ -396,7 +395,6 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese unzFile pkg = unzOpen2(template_path.utf8().get_data(), &io); if (!pkg) { - EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + template_path); return ERR_FILE_NOT_FOUND; } @@ -426,22 +424,18 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese //write if (file == "godot.html") { - if (!custom_html.empty()) { continue; } - _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug); + _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags); file = p_path.get_file(); } else if (file == "godot.js") { - file = p_path.get_file().get_basename() + ".js"; } else if (file == "godot.worker.js") { - file = p_path.get_file().get_basename() + ".worker.js"; } else if (file == "godot.wasm") { - file = p_path.get_file().get_basename() + ".wasm"; } @@ -459,7 +453,6 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese unzClose(pkg); if (!custom_html.empty()) { - FileAccess *f = FileAccess::open(custom_html, FileAccess::READ); if (!f) { EditorNode::get_singleton()->show_warning(TTR("Could not read custom HTML shell:") + "\n" + custom_html); @@ -469,7 +462,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese buf.resize(f->get_len()); f->get_buffer(buf.ptrw(), buf.size()); memdelete(f); - _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug); + _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags); f = FileAccess::open(p_path, FileAccess::WRITE); if (!f) { @@ -523,11 +516,9 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } bool EditorExportPlatformJavaScript::poll_export() { - Ref<EditorExportPreset> preset; for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) { - Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i); if (ep->is_runnable() && ep->get_platform() == this) { preset = ep; @@ -553,12 +544,10 @@ Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) c } int EditorExportPlatformJavaScript::get_options_count() const { - return menu_options; } Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { - if (p_option == 1) { MutexLock lock(server_lock); server->stop(); @@ -606,7 +595,6 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese } Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const { - return run_icon; } @@ -622,7 +610,6 @@ void EditorExportPlatformJavaScript::_server_thread_poll(void *data) { } EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { - server.instance(); server_quit = false; server_thread = Thread::create(_server_thread_poll, this); @@ -636,10 +623,11 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { run_icon->create_from_image(img); Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) + if (theme.is_valid()) { stop_icon = theme->get_icon("Stop", "EditorIcons"); - else + } else { stop_icon.instance(); + } menu_options = 0; } @@ -652,7 +640,6 @@ EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() { } void register_javascript_exporter() { - EDITOR_DEF("export/web/http_host", "localhost"); EDITOR_DEF("export/web/http_port", 8060); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1")); diff --git a/platform/javascript/http_client.h.inc b/platform/javascript/http_client.h.inc index ac275aadbc..4d5ff88bdd 100644 --- a/platform/javascript/http_client.h.inc +++ b/platform/javascript/http_client.h.inc @@ -33,21 +33,21 @@ Error prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers); int xhr_id; -int read_limit; -int response_read_offset; -Status status; +int read_limit = 4096; +int response_read_offset = 0; +Status status = STATUS_DISCONNECTED; String host; -int port; -bool use_tls; +int port = -1; +bool use_tls = false; String username; String password; -int polled_response_code; +int polled_response_code = 0; String polled_response_header; PackedByteArray polled_response; #ifdef DEBUG_ENABLED -bool has_polled; -uint64_t last_polling_frame; +bool has_polled = false; +uint64_t last_polling_frame = 0; #endif diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index 863c207896..cb0e48b8a9 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -29,10 +29,10 @@ /*************************************************************************/ #include "core/io/http_client.h" + #include "http_request.h" Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, bool p_verify_host) { - close(); if (p_ssl && !p_verify_host) { WARN_PRINT("Disabling HTTPClient's host verification is not supported for the HTML5 platform, host will be verified"); @@ -67,17 +67,14 @@ Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, } void HTTPClient::set_connection(const Ref<StreamPeer> &p_connection) { - ERR_FAIL_MSG("Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform."); } Ref<StreamPeer> HTTPClient::get_connection() const { - ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform."); } Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers) { - ERR_FAIL_INDEX_V(p_method, METHOD_MAX, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V_MSG(p_method == METHOD_TRACE || p_method == METHOD_CONNECT, ERR_UNAVAILABLE, "HTTP methods TRACE and CONNECT are not supported for the HTML5 platform."); ERR_FAIL_COND_V(status != STATUS_CONNECTED, ERR_INVALID_PARAMETER); @@ -104,7 +101,6 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve } Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body) { - Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; @@ -113,7 +109,6 @@ Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector } Error HTTPClient::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const String &p_body) { - Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; @@ -122,7 +117,6 @@ Error HTTPClient::request(Method p_method, const String &p_url, const Vector<Str } void HTTPClient::close() { - host = ""; port = -1; use_tls = false; @@ -134,28 +128,23 @@ void HTTPClient::close() { } HTTPClient::Status HTTPClient::get_status() const { - return status; } bool HTTPClient::has_response() const { - return !polled_response_header.empty(); } bool HTTPClient::is_response_chunked() const { - // TODO evaluate using moz-chunked-arraybuffer, fetch & ReadableStream return false; } int HTTPClient::get_response_code() const { - return polled_response_code; } Error HTTPClient::get_response_headers(List<String> *r_response) { - if (polled_response_header.empty()) return ERR_INVALID_PARAMETER; @@ -168,12 +157,10 @@ Error HTTPClient::get_response_headers(List<String> *r_response) { } int HTTPClient::get_response_body_length() const { - return polled_response.size(); } PackedByteArray HTTPClient::read_response_body_chunk() { - ERR_FAIL_COND_V(status != STATUS_BODY, PackedByteArray()); int to_read = MIN(read_limit, polled_response.size() - response_read_offset); @@ -192,17 +179,14 @@ PackedByteArray HTTPClient::read_response_body_chunk() { } void HTTPClient::set_blocking_mode(bool p_enable) { - ERR_FAIL_COND_MSG(p_enable, "HTTPClient blocking mode is not supported for the HTML5 platform."); } bool HTTPClient::is_blocking_mode_enabled() const { - return false; } void HTTPClient::set_read_chunk_size(int p_size) { - read_limit = p_size; } @@ -211,9 +195,7 @@ int HTTPClient::get_read_chunk_size() const { } Error HTTPClient::poll() { - switch (status) { - case STATUS_DISCONNECTED: return ERR_UNCONFIGURED; @@ -233,7 +215,6 @@ Error HTTPClient::poll() { return ERR_CONNECTION_ERROR; case STATUS_REQUESTING: { - #ifdef DEBUG_ENABLED if (!has_polled) { has_polled = true; @@ -279,20 +260,9 @@ Error HTTPClient::poll() { } HTTPClient::HTTPClient() { - xhr_id = godot_xhr_new(); - read_limit = 4096; - status = STATUS_DISCONNECTED; - port = -1; - use_tls = false; - polled_response_code = 0; -#ifdef DEBUG_ENABLED - has_polled = false; - last_polling_frame = 0; -#endif } HTTPClient::~HTTPClient() { - godot_xhr_free(xhr_id); } diff --git a/platform/javascript/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index db8050b90e..3a72b10dd4 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -34,14 +34,12 @@ #include "emscripten.h" extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { - p_arr->resize(p_len); *r_write = p_arr->write; return p_arr->ptrw(); } Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - union { bool b; double d; diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 815bc7e456..99672745e7 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -30,42 +30,123 @@ #include "core/io/resource_loader.h" #include "main/main.h" -#include "os_javascript.h" +#include "platform/javascript/display_server_javascript.h" +#include "platform/javascript/os_javascript.h" #include <emscripten/emscripten.h> -extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { +static OS_JavaScript *os = nullptr; +static uint64_t target_ticks = 0; + +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() { + uint64_t current_ticks = os->get_ticks_usec(); + bool force_draw = DisplayServerJavaScript::get_singleton()->check_size_force_redraw(); + if (force_draw) { + Main::force_redraw(); + } else if (current_ticks < target_ticks) { + return; // Skip frame. + } + + int target_fps = Engine::get_singleton()->get_target_fps(); + if (target_fps > 0) { + target_ticks += (uint64_t)(1000000 / target_fps); + } + if (os->main_loop_iterate()) { + emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. + /* clang-format off */ + EM_ASM({ + // This will contain the list of operations that need to complete before cleanup. + Module.async_finish = [ + // Always contains at least one async promise, to avoid firing immediately if nothing is added. + new Promise(function(accept, reject) { + setTimeout(accept, 0); + }) + ]; + }); + /* clang-format on */ + 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(); + // Immediately run the first iteration. + // We are inside an animation frame, we want to immediately draw on the newly setup canvas. + main_loop_callback(); + emscripten_resume_main_loop(); } int main(int argc, char *argv[]) { + // Create and mount userfs immediately. + EM_ASM({ + FS.mkdir('/userfs'); + FS.mount(IDBFS, {}, '/userfs'); + }); + + // Configure locale. + char locale_ptr[16]; + /* clang-format off */ + EM_ASM({ + stringToUTF8(Module['locale'], $0, 16); + }, locale_ptr); + /* clang-format on */ + setenv("LANG", locale_ptr, true); + + // Ensure the canvas ID. + /* clang-format off */ + EM_ASM({ + stringToUTF8("#" + Module['canvas'].id, $0, 255); + }, DisplayServerJavaScript::canvas_id); + /* clang-format on */ + + 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 */ - EM_ASM( - FS.mkdir('/userfs'); - FS.mount(IDBFS, {}, '/userfs'); + EM_ASM({ FS.syncfs(true, function(err) { - ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]) + requestAnimationFrame(function() { + ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]); + }); }); - ); + }); /* 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. } diff --git a/platform/javascript/http_request.js b/platform/javascript/native/http_request.js index f621689f9d..f621689f9d 100644 --- a/platform/javascript/http_request.js +++ b/platform/javascript/native/http_request.js diff --git a/platform/javascript/id_handler.js b/platform/javascript/native/id_handler.js index 67d29075b8..67d29075b8 100644 --- a/platform/javascript/id_handler.js +++ b/platform/javascript/native/id_handler.js diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js new file mode 100644 index 0000000000..95585d26ae --- /dev/null +++ b/platform/javascript/native/utils.js @@ -0,0 +1,204 @@ +/*************************************************************************/ +/* utils.js */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +Module['copyToFS'] = function(path, buffer) { + var p = path.lastIndexOf("/"); + var dir = "/"; + if (p > 0) { + dir = path.slice(0, path.lastIndexOf("/")); + } + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h + throw e; + } + FS.mkdirTree(dir); + } + // With memory growth, canOwn should be false. + FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); +} + +Module.drop_handler = (function() { + var upload = []; + var uploadPromises = []; + var uploadCallback = null; + + function readFilePromise(entry, path) { + return new Promise(function(resolve, reject) { + entry.file(function(file) { + var reader = new FileReader(); + reader.onload = function() { + var f = { + "path": file.relativePath || file.webkitRelativePath, + "name": file.name, + "type": file.type, + "size": file.size, + "data": reader.result + }; + if (!f['path']) + f['path'] = f['name']; + upload.push(f); + resolve() + }; + reader.onerror = function() { + console.log("Error reading file"); + reject(); + } + + reader.readAsArrayBuffer(file); + + }, function(err) { + console.log("Error!"); + reject(); + }); + }); + } + + function readDirectoryPromise(entry) { + return new Promise(function(resolve, reject) { + var reader = entry.createReader(); + reader.readEntries(function(entries) { + for (var i = 0; i < entries.length; i++) { + var ent = entries[i]; + if (ent.isDirectory) { + uploadPromises.push(readDirectoryPromise(ent)); + } else if (ent.isFile) { + uploadPromises.push(readFilePromise(ent)); + } + } + resolve(); + }); + }); + } + + function processUploadsPromises(resolve, reject) { + if (uploadPromises.length == 0) { + resolve(); + return; + } + uploadPromises.pop().then(function() { + setTimeout(function() { + processUploadsPromises(resolve, reject); + //processUploadsPromises.bind(null, resolve, reject) + }, 0); + }); + } + + function dropFiles(files) { + var args = files || []; + var argc = args.length; + var argv = stackAlloc((argc + 1) * 4); + for (var i = 0; i < argc; i++) { + HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]); + } + HEAP32[(argv >> 2) + argc] = 0; + // Defined in display_server_javascript.cpp + ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]); + } + + return function(ev) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (var i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + var entry = null; + if ("getAsEntry" in item) { + entry = item.getAsEntry(); + } else if ("webkitGetAsEntry" in item) { + entry = item.webkitGetAsEntry(); + } + if (!entry) { + console.error("File upload not supported"); + } else if (entry.isDirectory) { + uploadPromises.push(readDirectoryPromise(entry)); + } else if (entry.isFile) { + uploadPromises.push(readFilePromise(entry)); + } else { + console.error("Unrecognized entry...", entry); + } + } + } else { + console.error("File upload not supported"); + } + uploadCallback = new Promise(processUploadsPromises).then(function() { + const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; + var drops = []; + var files = []; + upload.forEach((elem) => { + var path = elem['path']; + Module['copyToFS'](DROP + path, elem['data']); + var idx = path.indexOf("/"); + if (idx == -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + var sub = path.substr(0, idx); + idx = sub.indexOf("/"); + if (idx < 0 && drops.indexOf(DROP + sub) == -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + uploadPromises = []; + upload = []; + dropFiles(drops); + var dirs = [DROP.substr(0, DROP.length -1)]; + files.forEach(function (file) { + FS.unlink(file); + var dir = file.replace(DROP, ""); + var idx = dir.lastIndexOf("/"); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(DROP + dir) == -1) { + dirs.push(DROP + dir); + } + idx = dir.lastIndexOf("/"); + } + }); + // Remove dirs. + dirs = dirs.sort(function(a, b) { + var al = (a.match(/\//g) || []).length; + var bl = (b.match(/\//g) || []).length; + if (al > bl) + return -1; + else if (al < bl) + return 1; + return 0; + }); + dirs.forEach(function(dir) { + FS.rmdir(dir); + }); + }); + } +})(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index ad06aef86e..1ff4304bcf 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -30,668 +30,23 @@ #include "os_javascript.h" +#include "core/debugger/engine_debugger.h" #include "core/io/file_access_buffered_fa.h" -//#include "drivers/gles2/rasterizer_gles2.h" -#include "drivers/dummy/rasterizer_dummy.h" +#include "core/io/json.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" -#include "servers/rendering/rendering_server_raster.h" +#include "modules/modules_enabled.gen.h" +#include "platform/javascript/display_server_javascript.h" + +#ifdef MODULE_WEBSOCKET_ENABLED +#include "modules/websocket/remote_debugger_peer_websocket.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 -#define GODOT_CANVAS_SELECTOR "#canvas" - -// 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_V( - return document.activeElement == Module.canvas; - ); - /* clang-format on */ -} - -static Point2 compute_position_in_canvas(int x, int y) { - int canvas_x = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().x; - }); - int canvas_y = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().y; - }); - int canvas_width; - int canvas_height; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, &canvas_width, &canvas_height); - - double element_width; - double element_height; - emscripten_get_element_css_size(GODOT_CANVAS_SELECTOR, &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 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(GODOT_CANVAS_SELECTOR, p_size.x, p_size.y); - } -} - -Size2 OS_JavaScript::get_window_size() const { - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, 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 = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &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 = nullptr; - EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, 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; -} - -// 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 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_keycode())) { - // 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->audio_driver_javascript.resume(); - 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_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; -} - -// Mouse - -Point2 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->audio_driver_javascript.resume(); - // 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<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()) { - 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; - - 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); - } - - 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; -} - -// Wheel - -EM_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; -} - -// Touch - bool OS_JavaScript::has_touchscreen_ui_hint() const { - /* clang-format off */ return EM_ASM_INT_V( return 'ontouchstart' in window; @@ -699,352 +54,47 @@ bool OS_JavaScript::has_touchscreen_ui_hint() const { /* 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->audio_driver_javascript.resume(); - 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; -} - -// Gamepad - -EM_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); -} - -// Video - -int 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_GLES2: - return "GLES2"; - } - ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); -} - // Audio int OS_JavaScript::get_audio_driver_count() const { - return 1; } const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { - return "JavaScript"; } -// Clipboard -extern "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(); -} - // Lifecycle -int OS_JavaScript::get_current_video_driver() const { - return video_driver_index; -} - -void OS_JavaScript::initialize_core() { - +void OS_JavaScript::initialize() { 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) { - -#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; - } + DisplayServerJavaScript::register_javascript_driver(); - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &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; - - 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_V({ return Module.resizeCanvasOnStart })) { - /* clang-format on */ - set_window_size(Size2(video_mode.width, video_mode.height)); - } else { - set_window_size(get_window_size()); - } +#ifdef MODULE_WEBSOCKET_ENABLED + EngineDebugger::register_uri_handler("ws://", RemoteDebuggerPeerWebSocket::create); + EngineDebugger::register_uri_handler("wss://", RemoteDebuggerPeerWebSocket::create); #endif - RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? - - char locale_ptr[16]; - /* clang-format off */ - EM_ASM_ARGS({ - stringToUTF8(Module.locale, $0, 16); - }, locale_ptr); - /* clang-format on */ - setenv("LANG", locale_ptr, true); - - AudioDriverManager::initialize(p_audio_driver); - RenderingServer *rendering_server = memnew(RenderingServerRaster()); - input = memnew(InputDefault); - - EMSCRIPTEN_RESULT result; -#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(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, wheel, wheel_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, 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({ - const send_notification = cwrap('send_notification', null, ['number']); - const notifications = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index])); - }); - // Clipboard - const update_clipboard = cwrap('update_clipboard', null, ['string']); - window.addEventListener('paste', function(evt) { - update_clipboard(evt.clipboardData.getData('text')); - }, true); - }, - NOTIFICATION_WM_MOUSE_ENTER, - NOTIFICATION_WM_MOUSE_EXIT, - NOTIFICATION_WM_FOCUS_IN, - NOTIFICATION_WM_FOCUS_OUT - ); - /* clang-format on */ - - rendering_server->init(); +} - return OK; +void OS_JavaScript::resume_audio() { + if (audio_driver_javascript) { + audio_driver_javascript->resume(); + } } 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::run_async() { - - main_loop->init(); - emscripten_set_main_loop(main_loop_callback, -1, false); -} - void OS_JavaScript::main_loop_callback() { - get_singleton()->main_loop_iterate(); } 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; @@ -1063,72 +113,65 @@ bool OS_JavaScript::main_loop_iterate() { } } - 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 = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); - } else { - emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height); - } - just_exited_fullscreen = false; - } - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, 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]; - } + DisplayServer::get_singleton()->process_events(); return Main::iteration(); } void OS_JavaScript::delete_main_loop() { + if (main_loop) { + memdelete(main_loop); + } + main_loop = nullptr; +} - memdelete(main_loop); +void OS_JavaScript::finalize_async() { + finalizing = true; + if (audio_driver_javascript) { + audio_driver_javascript->finish_async(); + } } void OS_JavaScript::finalize() { - - memdelete(input); + delete_main_loop(); + if (audio_driver_javascript) { + memdelete(audio_driver_javascript); + audio_driver_javascript = nullptr; + } } // Miscellaneous Error 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) { - - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::execute() is not available on the HTML5 platform."); + 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 == NOTIFICATION_WM_MOUSE_ENTER || p_notification == NOTIFICATION_WM_MOUSE_EXIT) { - cursor_inside_canvas = p_notification == NOTIFICATION_WM_MOUSE_ENTER; - } - OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification); -} - bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { - if (p_feature == "HTML5" || p_feature == "web") return true; @@ -1140,79 +183,11 @@ bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { 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; - - 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_ARGS({ - 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 */ -} - 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_({ @@ -1223,27 +198,30 @@ Error OS_JavaScript::shell_open(String p_uri) { } 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_resource_dir() const { +String OS_JavaScript::get_cache_path() const { + return "/home/web_user/.cache"; +} - return "/"; +String OS_JavaScript::get_config_path() const { + return "/home/web_user/.config"; } -void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { +String OS_JavaScript::get_data_path() const { + return "/home/web_user/.local/share"; +} +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(); @@ -1253,43 +231,25 @@ void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags } 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[]) { +void OS_JavaScript::initialize_joypads() { +} - List<String> arguments; - for (int i = 1; i < p_argc; i++) { - arguments.push_back(String::utf8(p_argv[i])); +OS_JavaScript::OS_JavaScript() { + if (AudioDriverJavaScript::is_available()) { + audio_driver_javascript = memnew(AudioDriverJavaScript); + AudioDriverManager::add_driver(audio_driver_javascript); } - set_cmdline(p_argv[0], arguments); - - 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 = nullptr; - - idb_available = false; - sync_wait_time = -1; - - AudioDriverManager::add_driver(&audio_driver_javascript); Vector<Logger *> loggers; loggers.push_back(memnew(StdLogger)); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index feb0f0651a..22234f9355 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -35,64 +35,24 @@ #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 { + MainLoop *main_loop = nullptr; + AudioDriverJavaScript *audio_driver_javascript = nullptr; - 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; - 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 +65,41 @@ 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; + // Override default OS implementation which would block the main thread with delay_usec. + // Implemented in javascript_main.cpp loop callback instead. + virtual void add_frame_delay(bool p_can_draw) {} 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 |