diff --git a/platform/javascript/dom_keys.h b/platform/javascript/dom_keys.inc similarity index 97% rename from platform/javascript/dom_keys.h rename to platform/javascript/dom_keys.inc index 4edca63c6de..dc8d67d52b9 100644 --- a/platform/javascript/dom_keys.h +++ b/platform/javascript/dom_keys.inc @@ -1,5 +1,5 @@ /*************************************************************************/ -/* dom_keys.h */ +/* dom_keys.inc */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,9 +28,6 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef DOM_KEYS_H -#define DOM_KEYS_H - #include "os/keyboard.h" // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Constants_for_keyCode_value @@ -295,8 +292,8 @@ int dom2godot_scancode(int dom_keycode) { //case DOM_VK_SELECT: return KEY_UNKNOWN; - case DOM_VK_PRINTSCREEN: // this is the usual printScreen key - case DOM_VK_PRINT: // maybe for alt+printScreen or physical printers? + case DOM_VK_PRINTSCREEN: + case DOM_VK_PRINT: return KEY_PRINT; //case DOM_VK_EXECUTE: return KEY_UNKNOWN; @@ -311,11 +308,11 @@ int dom2godot_scancode(int dom_keycode) { case DOM_VK_SLEEP: return KEY_STANDBY; - // these are numpad keys according to MDN + // 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? + 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: @@ -376,10 +373,8 @@ int dom2godot_scancode(int dom_keycode) { case DOM_VK_QUOTE: return KEY_APOSTROPHE; - // rest is OEM/unusual + // The rest is OEM/unusual. default: return KEY_UNKNOWN; }; } - -#endif diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 68a2d724642..3829e8d4068 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -28,17 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "emscripten.h" #include "io/resource_loader.h" #include "main/main.h" #include "os_javascript.h" -OS_JavaScript *os = NULL; - -static void main_loop() { - - os->main_loop_iterate(); -} +#include extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { @@ -46,18 +40,18 @@ extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { if (!idbfs_err.empty()) { print_line("IndexedDB not available: " + idbfs_err); } - os->set_idbfs_available(idbfs_err.empty()); - // Ease up compatibility + OS_JavaScript *os = OS_JavaScript::get_singleton(); + os->set_idb_available(idbfs_err.empty()); + // Ease up compatibility. ResourceLoader::set_abort_on_missing_resources(false); Main::start(); - os->main_loop_begin(); - emscripten_set_main_loop(main_loop, 0, false); + os->run_async(); } int main(int argc, char *argv[]) { - // sync from persistent state into memory and then - // run the 'main_after_fs_sync' function + // Sync from persistent state into memory and then + // run the 'main_after_fs_sync' function. /* clang-format off */ EM_ASM( FS.mkdir('/userfs'); @@ -68,9 +62,10 @@ int main(int argc, char *argv[]) { ); /* clang-format on */ - os = new OS_JavaScript(argv[0], NULL); - Error err = Main::setup(argv[0], argc - 1, &argv[1]); + 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 syncfs() callback + // Continued async in main_after_fs_sync() from the syncfs() callback. } diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index d99bc69f003..c05ae03ec6e 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -30,107 +30,26 @@ #include "os_javascript.h" -#include "core/engine.h" -#include "core/io/file_access_buffered_fa.h" -#include "dom_keys.h" -#include "drivers/gles2/rasterizer_gles2.h" -#include "drivers/gles3/rasterizer_gles3.h" -#include "drivers/unix/dir_access_unix.h" -#include "drivers/unix/file_access_unix.h" +#include "gles2/rasterizer_gles2.h" +#include "gles3/rasterizer_gles3.h" +#include "io/file_access_buffered_fa.h" #include "main/main.h" #include "servers/visual/visual_server_raster.h" +#include "unix/dir_access_unix.h" +#include "unix/file_access_unix.h" #include #include +#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 -template -static void dom2godot_mod(T emscripten_event_ptr, Ref 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); -} - -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_GLES3: - return "GLES3"; - case VIDEO_DRIVER_GLES2: - return "GLES2"; - } - ERR_EXPLAIN("Invalid video driver index " + itos(p_driver)); - ERR_FAIL_V(NULL); -} - -int OS_JavaScript::get_audio_driver_count() const { - - return 1; -} - -const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { - - return "JavaScript"; -} - -void OS_JavaScript::initialize_core() { - - OS_Unix::initialize_core(); - FileAccess::make_default >(FileAccess::ACCESS_RESOURCES); -} - -static EM_BOOL _browser_resize_callback(int event_type, const EmscriptenUiEvent *ui_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_RESIZE, false); - - OS_JavaScript *os = static_cast(user_data); - // The order of the fullscreen change event and the window size change - // event varies, even within just one browser, so defer handling - os->request_canvas_size_adjustment(); - return false; -} - -static EM_BOOL _fullscreen_change_callback(int event_type, const EmscriptenFullscreenChangeEvent *event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_FULLSCREENCHANGE, false); - - OS_JavaScript *os = static_cast(user_data); - String id = String::utf8(event->id); - // empty id is canvas - if (id.empty() || id == "canvas") { - - OS::VideoMode vm = os->get_video_mode(); - // this event property is the only reliable information on - // browser fullscreen state - vm.fullscreen = event->isFullscreen; - os->set_video_mode(vm); - os->request_canvas_size_adjustment(); - } - return false; -} - -static InputDefault *_input; - -static bool is_canvas_focused() { - - /* clang-format off */ - return EM_ASM_INT_V( - return document.activeElement == Module.canvas; - ); - /* clang-format on */ -} +// Window (canvas) static void focus_canvas() { @@ -141,479 +60,39 @@ static void focus_canvas() { /* clang-format on */ } -static bool _cursor_inside_canvas = true; +static bool is_canvas_focused() { -static bool is_cursor_inside_canvas() { - - return _cursor_inside_canvas; + /* clang-format off */ + return EM_ASM_INT_V( + return document.activeElement == Module.canvas; + ); + /* clang-format on */ } -static EM_BOOL _mousebutton_callback(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) { +static bool cursor_inside_canvas = true; - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_MOUSEDOWN && event_type != EMSCRIPTEN_EVENT_MOUSEUP, false); +EM_BOOL OS_JavaScript::browser_resize_callback(int p_event_type, const EmscriptenUiEvent *p_event, void *p_user_data) { - Ref ev; - ev.instance(); - ev->set_pressed(event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); - ev->set_position(Point2(mouse_event->canvasX, mouse_event->canvasY)); - ev->set_global_position(ev->get_position()); - dom2godot_mod(mouse_event, ev); - switch (mouse_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; - } - - 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 used, 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 { - // 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; -} - -static EM_BOOL _mousemove_callback(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_MOUSEMOVE, false); - OS_JavaScript *os = static_cast(user_data); - int input_mask = _input->get_mouse_button_mask(); - Point2 pos = Point2(mouse_event->canvasX, mouse_event->canvasY); - // outside the canvas, only read mouse movement if dragging started inside - // the canvas; imitating desktop app behaviour - if (!is_cursor_inside_canvas() && !input_mask) - return false; - - Ref ev; - ev.instance(); - dom2godot_mod(mouse_event, ev); - ev->set_button_mask(input_mask); - - ev->set_position(pos); - ev->set_global_position(ev->get_position()); - - ev->set_relative(Vector2(mouse_event->movementX, mouse_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 + // The order of the fullscreen change event and the window size change + // event varies, even within just one browser, so defer handling. + get_singleton()->canvas_size_adjustment_requested = true; return false; } -static EM_BOOL _wheel_callback(int event_type, const EmscriptenWheelEvent *wheel_event, void *user_data) { +EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_WHEEL, false); - if (!is_canvas_focused()) { - if (is_cursor_inside_canvas()) { - focus_canvas(); - } else { - return false; - } - } - - Ref ev; - ev.instance(); - ev->set_button_mask(_input->get_mouse_button_mask()); - 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 (wheel_event->deltaY < 0) - ev->set_button_index(BUTTON_WHEEL_UP); - else if (wheel_event->deltaY > 0) - ev->set_button_index(BUTTON_WHEEL_DOWN); - else if (wheel_event->deltaX > 0) - ev->set_button_index(BUTTON_WHEEL_LEFT); - else if (wheel_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 - - ev->set_pressed(true); - _input->parse_input_event(ev); - - ev->set_pressed(false); - _input->parse_input_event(ev); - - return true; -} - -static Point2 _prev_touches[32]; - -static EM_BOOL _touchpress_callback(int event_type, const EmscriptenTouchEvent *touch_event, void *user_data) { - - ERR_FAIL_COND_V( - event_type != EMSCRIPTEN_EVENT_TOUCHSTART && - event_type != EMSCRIPTEN_EVENT_TOUCHEND && - event_type != EMSCRIPTEN_EVENT_TOUCHCANCEL, - false); - - Ref ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < touch_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = touch_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < touch_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(Point2(touch.canvasX, touch.canvasY)); - _prev_touches[i] = ev->get_position(); - ev->set_pressed(event_type == EMSCRIPTEN_EVENT_TOUCHSTART); - - _input->parse_input_event(ev); - } - return true; -} - -static EM_BOOL _touchmove_callback(int event_type, const EmscriptenTouchEvent *touch_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_TOUCHMOVE, false); - - Ref ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < touch_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = touch_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < touch_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(Point2(touch.canvasX, touch.canvasY)); - Point2 &prev = _prev_touches[i]; - ev->set_relative(ev->get_position() - prev); - prev = ev->get_position(); - - _input->parse_input_event(ev); - } - return true; -} - -static Ref _setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { - - Ref ev; - ev.instance(); - ev->set_echo(emscripten_event->repeat); - dom2godot_mod(emscripten_event, ev); - ev->set_scancode(dom2godot_scancode(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; -} - -static Ref deferred_key_event; - -static EM_BOOL _keydown_callback(int event_type, const EmscriptenKeyboardEvent *key_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_KEYDOWN, false); - - Ref ev = _setup_key_event(key_event); - ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_scancode())) { - // defer to keypress event for legacy unicode retrieval - deferred_key_event = ev; - return false; // do not suppress keypress event - } - _input->parse_input_event(ev); - return true; -} - -static EM_BOOL _keypress_callback(int event_type, const EmscriptenKeyboardEvent *key_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_KEYPRESS, false); - - deferred_key_event->set_unicode(key_event->charCode); - _input->parse_input_event(deferred_key_event); - return true; -} - -static EM_BOOL _keyup_callback(int event_type, const EmscriptenKeyboardEvent *key_event, void *user_data) { - - ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_KEYUP, false); - - Ref ev = _setup_key_event(key_event); - ev->set_pressed(false); - _input->parse_input_event(ev); - return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0; -} - -static EM_BOOL joy_callback_func(int p_type, const EmscriptenGamepadEvent *p_event, void *p_user) { - OS_JavaScript *os = (OS_JavaScript *)OS::get_singleton(); - if (os) { - return os->joy_connection_changed(p_type, p_event); + 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; + os->canvas_size_adjustment_requested = true; } return false; } -extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int notif) { - - if (notif == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || notif == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) { - _cursor_inside_canvas = notif == MainLoop::NOTIFICATION_WM_MOUSE_ENTER; - } - OS_JavaScript::get_singleton()->get_main_loop()->notification(notif); -} - -Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - - print_line("Init OS"); - - EmscriptenWebGLContextAttributes attributes; - emscripten_webgl_init_context_attributes(&attributes); - attributes.alpha = false; - attributes.antialias = false; - ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); - switch (p_video_driver) { - case VIDEO_DRIVER_GLES3: - attributes.majorVersion = 2; - RasterizerGLES3::register_config(); - RasterizerGLES3::make_current(); - break; - case VIDEO_DRIVER_GLES2: - attributes.majorVersion = 1; - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - break; - } - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(NULL, &attributes); - ERR_EXPLAIN("WebGL " + itos(attributes.majorVersion) + ".0 not available"); - ERR_FAIL_COND_V(emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS, ERR_UNAVAILABLE); - - video_mode = p_desired; - // can't fulfil fullscreen request due to browser security - video_mode.fullscreen = false; - /* 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()); - } - - 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); - - print_line("Init Audio"); - - AudioDriverManager::initialize(p_audio_driver); - - print_line("Init VS"); - - visual_server = memnew(VisualServerRaster()); - // visual_server->cursor_set_visible(false, 0); - - print_line("Init Physicsserver"); - - input = memnew(InputDefault); - _input = input; - -#define EM_CHECK(ev) \ - if (result != EMSCRIPTEN_RESULT_SUCCESS) \ - ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result)) -#define SET_EM_CALLBACK(target, ev, cb) \ - result = emscripten_set_##ev##_callback(target, this, true, &cb); \ - EM_CHECK(ev) -#define SET_EM_CALLBACK_NODATA(ev, cb) \ - result = emscripten_set_##ev##_callback(NULL, true, &cb); \ - EM_CHECK(ev) - - EMSCRIPTEN_RESULT result; - SET_EM_CALLBACK("#window", mousemove, _mousemove_callback) - SET_EM_CALLBACK("#canvas", mousedown, _mousebutton_callback) - SET_EM_CALLBACK("#window", mouseup, _mousebutton_callback) - SET_EM_CALLBACK("#window", wheel, _wheel_callback) - SET_EM_CALLBACK("#window", touchstart, _touchpress_callback) - SET_EM_CALLBACK("#window", touchmove, _touchmove_callback) - SET_EM_CALLBACK("#window", touchend, _touchpress_callback) - SET_EM_CALLBACK("#window", touchcancel, _touchpress_callback) - SET_EM_CALLBACK("#canvas", keydown, _keydown_callback) - SET_EM_CALLBACK("#canvas", keypress, _keypress_callback) - SET_EM_CALLBACK("#canvas", keyup, _keyup_callback) - SET_EM_CALLBACK(NULL, resize, _browser_resize_callback) - SET_EM_CALLBACK(NULL, fullscreenchange, _fullscreen_change_callback) - SET_EM_CALLBACK_NODATA(gamepadconnected, joy_callback_func) - SET_EM_CALLBACK_NODATA(gamepaddisconnected, joy_callback_func) - -#undef SET_EM_CALLBACK_NODATA -#undef SET_EM_CALLBACK -#undef EM_CHECK - - visual_server->init(); - - return OK; -} - -void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) { - - main_loop = p_main_loop; - input->set_main_loop(p_main_loop); -} - -void OS_JavaScript::delete_main_loop() { - - memdelete(main_loop); -} - -void OS_JavaScript::finalize() { - - memdelete(input); -} - -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 */ -} - -static const char *godot2dom_cursor(OS::CursorShape p_shape) { - - switch (p_shape) { - case OS::CURSOR_ARROW: - default: - return "auto"; - case OS::CURSOR_IBEAM: return "text"; - case OS::CURSOR_POINTING_HAND: return "pointer"; - case OS::CURSOR_CROSS: return "crosshair"; - case OS::CURSOR_WAIT: return "progress"; - case OS::CURSOR_BUSY: return "wait"; - case OS::CURSOR_DRAG: return "grab"; - case OS::CURSOR_CAN_DROP: return "grabbing"; - case OS::CURSOR_FORBIDDEN: return "no-drop"; - case OS::CURSOR_VSIZE: return "ns-resize"; - case OS::CURSOR_HSIZE: return "ew-resize"; - case OS::CURSOR_BDIAGSIZE: return "nesw-resize"; - case OS::CURSOR_FDIAGSIZE: return "nwse-resize"; - case OS::CURSOR_MOVE: return "move"; - case OS::CURSOR_VSPLIT: return "row-resize"; - case OS::CURSOR_HSPLIT: return "col-resize"; - case OS::CURSOR_HELP: return "help"; - } -} - -void OS_JavaScript::set_css_cursor(const char *p_cursor) { - - /* clang-format off */ - EM_ASM_({ - Module.canvas.style.cursor = UTF8ToString($0); - }, p_cursor); - /* clang-format on */ -} - -const char *OS_JavaScript::get_css_cursor() const { - - char cursor[16]; - /* clang-format off */ - EM_ASM_INT({ - stringToUTF8(Module.canvas.style.cursor ? Module.canvas.style.cursor : 'auto', $0, 16); - }, cursor); - /* clang-format on */ - return cursor; -} - -void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) { - - ERR_FAIL_INDEX(p_mode, MOUSE_MODE_CONFINED + 1); - ERR_EXPLAIN("MOUSE_MODE_CONFINED is not supported for the HTML5 platform"); - ERR_FAIL_COND(p_mode == MOUSE_MODE_CONFINED); - if (p_mode == get_mouse_mode()) - return; - - if (p_mode == MOUSE_MODE_VISIBLE) { - - set_css_cursor(godot2dom_cursor(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_EXPLAIN("MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback"); - ERR_FAIL_COND(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED); - ERR_FAIL_COND(result != EMSCRIPTEN_RESULT_SUCCESS); - set_css_cursor(godot2dom_cursor(cursor_shape)); - } -} - -OS::MouseMode OS_JavaScript::get_mouse_mode() const { - - if (!strcmp(get_css_cursor(), "none")) - return MOUSE_MODE_HIDDEN; - - EmscriptenPointerlockChangeEvent ev; - emscripten_get_pointerlock_status(&ev); - return ev.isActive && (strcmp(ev.id, "canvas") == 0) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; -} - -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(); -} - -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 */ -} - -//interesting byt not yet -//void set_clipboard(const String& p_text); -//String get_clipboard() const; - void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) { video_mode = p_video_mode; @@ -662,8 +141,8 @@ void OS_JavaScript::set_window_maximized(bool p_enabled) { return; } // Calling emscripten_enter_soft_fullscreen mutltiple times hides all - // page elements except the canvas permanently, so track state - if (p_enabled && !soft_fs_enabled) { + // page elements except the canvas permanently, so track state. + if (p_enabled && !soft_fullscreen_enabled) { EmscriptenFullscreenStrategy strategy; strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; @@ -671,31 +150,36 @@ void OS_JavaScript::set_window_maximized(bool p_enabled) { strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; strategy.canvasResizedCallback = NULL; emscripten_enter_soft_fullscreen(NULL, &strategy); - soft_fs_enabled = true; + soft_fullscreen_enabled = true; video_mode.width = get_window_size().width; video_mode.height = get_window_size().height; } else if (!p_enabled) { emscripten_exit_soft_fullscreen(); - soft_fs_enabled = false; + soft_fullscreen_enabled = false; video_mode.width = windowed_size.width; video_mode.height = windowed_size.height; emscripten_set_canvas_size(video_mode.width, video_mode.height); } } -void OS_JavaScript::set_window_fullscreen(bool p_enable) { +bool OS_JavaScript::is_window_maximized() const { - if (p_enable == is_window_fullscreen()) { + return window_maximized; +} + +void OS_JavaScript::set_window_fullscreen(bool p_enabled) { + + if (p_enabled == is_window_fullscreen()) { return; } - // only requesting changes here, if successful, canvas is resized in - // _browser_resize_callback or _fullscreen_change_callback + // Just request changes here, if successful, canvas is resized in + // _browser_resize_callback or _fullscreen_change_callback. EMSCRIPTEN_RESULT result; - if (p_enable) { + if (p_enabled) { if (window_maximized) { - // soft fs during real fs can cause issues + // Soft fullsreen during real fulllscreen can cause issues. set_window_maximized(false); window_maximized = true; } @@ -718,30 +202,198 @@ bool OS_JavaScript::is_window_fullscreen() const { return video_mode.fullscreen; } -void OS_JavaScript::request_canvas_size_adjustment() { - - canvas_size_adjustment_requested = true; -} - void OS_JavaScript::get_fullscreen_mode_list(List *p_list, int p_screen) const { Size2 screen = get_screen_size(); p_list->push_back(OS::VideoMode(screen.width, screen.height, true)); } -String OS_JavaScript::get_name() { +// Keys - return "HTML5"; +template +static void dom2godot_mod(T *emscripten_event_ptr, Ref 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); } -MainLoop *OS_JavaScript::get_main_loop() const { +static Ref setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { - return main_loop; + Ref ev; + ev.instance(); + ev->set_echo(emscripten_event->repeat); + dom2godot_mod(emscripten_event, ev); + ev->set_scancode(dom2godot_scancode(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; } -bool OS_JavaScript::can_draw() const { +EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - return true; //always? + OS_JavaScript *os = get_singleton(); + Ref ev = setup_key_event(p_event); + ev->set_pressed(true); + if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_scancode())) { + // Defer to keypress event for legacy unicode retrieval. + os->deferred_key_event = ev; + // Do not suppress keypress event. + return false; + } + os->input->parse_input_event(ev); + 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 ev = setup_key_event(p_event); + ev->set_pressed(false); + get_singleton()->input->parse_input_event(ev); + return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0; +} + +// 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 ev; + ev.instance(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); + ev->set_position(Point2(p_event->canvasX, p_event->canvasY)); + 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; + } + + 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); + // 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 = Point2(p_event->canvasX, p_event->canvasY); + // 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 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 const char *get_css_cursor() { + + char cursor[16]; + /* clang-format off */ + EM_ASM_INT({ + stringToUTF8(Module.canvas.style.cursor ? Module.canvas.style.cursor : 'auto', $0, 16); + }, cursor); + /* clang-format on */ + return cursor; } void OS_JavaScript::set_cursor_shape(CursorShape p_shape) { @@ -756,17 +408,332 @@ void OS_JavaScript::set_cursor_shape(CursorShape p_shape) { void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { } -void OS_JavaScript::main_loop_begin() { +void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) { - if (main_loop) - main_loop->init(); + ERR_EXPLAIN("MOUSE_MODE_CONFINED is not supported for the HTML5 platform"); + ERR_FAIL_COND(p_mode == MOUSE_MODE_CONFINED); + if (p_mode == get_mouse_mode()) + return; + + if (p_mode == MOUSE_MODE_VISIBLE) { + + set_css_cursor(godot2dom_cursor(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_EXPLAIN("MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback"); + ERR_FAIL_COND(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED); + ERR_FAIL_COND(result != EMSCRIPTEN_RESULT_SUCCESS); + set_css_cursor(godot2dom_cursor(cursor_shape)); + } +} + +OS::MouseMode OS_JavaScript::get_mouse_mode() const { + + if (String::utf8(get_css_cursor()) == "none") + 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 ev; + ev.instance(); + ev->set_button_mask(input->get_mouse_button_mask()); + 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. + + ev->set_pressed(true); + input->parse_input_event(ev); + + ev->set_pressed(false); + 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; + ); + /* 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 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(Point2(touch.canvasX, touch.canvasY)); + os->touches[i] = ev->get_position(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); + + os->input->parse_input_event(ev); + } + 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 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(Point2(touch.canvasX, touch.canvasY)); + 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_get_gamepad_status(joypad, &state); + if (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_GLES3: + return "GLES3"; + case VIDEO_DRIVER_GLES2: + return "GLES2"; + } + ERR_EXPLAIN("Invalid video driver index " + itos(p_driver)); + ERR_FAIL_V(NULL); +} + +// 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"; +} + +// Lifecycle + +void OS_JavaScript::initialize_core() { + + OS_Unix::initialize_core(); + FileAccess::make_default >(FileAccess::ACCESS_RESOURCES); +} + +Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { + + EmscriptenWebGLContextAttributes attributes; + emscripten_webgl_init_context_attributes(&attributes); + attributes.alpha = false; + attributes.antialias = false; + ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); + switch (p_video_driver) { + case VIDEO_DRIVER_GLES3: + attributes.majorVersion = 2; + RasterizerGLES3::register_config(); + RasterizerGLES3::make_current(); + break; + case VIDEO_DRIVER_GLES2: + attributes.majorVersion = 1; + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + break; + } + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(NULL, &attributes); + ERR_EXPLAIN("WebGL " + itos(attributes.majorVersion) + ".0 not available"); + ERR_FAIL_COND_V(emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS, ERR_UNAVAILABLE); + + video_mode = p_desired; + // Can't fulfil fullscreen request during start-up due to browser security. + video_mode.fullscreen = false; + /* 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()); + } + + 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); + VisualServer *visual_server = memnew(VisualServerRaster()); + input = memnew(InputDefault); + + EMSCRIPTEN_RESULT result; +#define EM_CHECK(ev) \ + if (result != EMSCRIPTEN_RESULT_SUCCESS) \ + ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result)) +#define SET_EM_CALLBACK(target, ev, cb) \ + result = emscripten_set_##ev##_callback(target, NULL, true, &cb); \ + EM_CHECK(ev) +#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ + result = emscripten_set_##ev##_callback(NULL, true, &cb); \ + EM_CHECK(ev) + // These callbacks from Emscripten's html5.h suffice to access most + // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM + // is used below. + SET_EM_CALLBACK("#window", mousemove, mousemove_callback) + SET_EM_CALLBACK("#canvas", mousedown, mouse_button_callback) + SET_EM_CALLBACK("#window", mouseup, mouse_button_callback) + SET_EM_CALLBACK("#window", wheel, wheel_callback) + SET_EM_CALLBACK("#window", touchstart, touch_press_callback) + SET_EM_CALLBACK("#window", touchmove, touchmove_callback) + SET_EM_CALLBACK("#window", touchend, touch_press_callback) + SET_EM_CALLBACK("#window", touchcancel, touch_press_callback) + SET_EM_CALLBACK("#canvas", keydown, keydown_callback) + SET_EM_CALLBACK("#canvas", keypress, keypress_callback) + SET_EM_CALLBACK("#canvas", keyup, keyup_callback) + SET_EM_CALLBACK(NULL, resize, browser_resize_callback) + SET_EM_CALLBACK(NULL, fullscreenchange, fullscreen_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback) +#undef SET_EM_CALLBACK_NODATA +#undef SET_EM_CALLBACK +#undef EM_CHECK /* clang-format off */ EM_ASM_ARGS({ const send_notification = cwrap('send_notification', null, ['number']); - const notifs = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, i) { - Module.canvas.addEventListener(event, send_notification.bind(null, notifs[i])); + const notifications = arguments; + (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { + Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index])); }); }, MainLoop::NOTIFICATION_WM_MOUSE_ENTER, @@ -775,22 +742,44 @@ void OS_JavaScript::main_loop_begin() { MainLoop::NOTIFICATION_WM_FOCUS_OUT ); /* clang-format on */ + + visual_server->init(); + + return OK; +} + +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 (!main_loop) - return false; + if (is_userfs_persistent() && sync_wait_time >= 0) { + int64_t current_time = get_ticks_msec(); + int64_t elapsed_time = current_time - last_sync_check_time; + last_sync_check_time = current_time; - if (idbfs_available && time_to_save_sync >= 0) { - int64_t newtime = get_ticks_msec(); - int64_t elapsed = newtime - last_sync_time; - last_sync_time = newtime; + sync_wait_time -= elapsed_time; - time_to_save_sync -= elapsed; - - if (time_to_save_sync < 0) { - //time to sync, for real + if (sync_wait_time < 0) { /* clang-format off */ EM_ASM( FS.syncfs(function(err) { @@ -815,33 +804,75 @@ bool OS_JavaScript::main_loop_iterate() { return Main::iteration(); } -void OS_JavaScript::main_loop_end() { +void OS_JavaScript::delete_main_loop() { - if (main_loop) - main_loop->finish(); + memdelete(main_loop); } -void OS_JavaScript::process_accelerometer(const Vector3 &p_accelerometer) { +void OS_JavaScript::finalize() { - input->set_accelerometer(p_accelerometer); + memdelete(input); } -bool OS_JavaScript::has_touchscreen_ui_hint() const { +// Miscellaneous + +extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) { + + if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) { + cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER; + } + 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; + +#ifdef JAVASCRIPT_EVAL_ENABLED + if (p_feature == "JavaScript") + return true; +#endif + + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_get_current_context(); + // All extensions are already automatically enabled, this function allows + // checking WebGL extension support without inline JavaScript + if (p_feature == "s3tc") + return emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_s3tc_srgb"); + if (p_feature == "etc") + return emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_etc1"); + if (p_feature == "etc2") + return emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_etc"); + + return false; +} + +void OS_JavaScript::alert(const String &p_alert, const String &p_title) { /* clang-format off */ - return EM_ASM_INT_V( - return 'ontouchstart' in window; - ); + EM_ASM_({ + window.alert(UTF8ToString($0)); + }, p_alert.utf8().get_data()); /* clang-format on */ } -void OS_JavaScript::main_loop_request_quit() { +void OS_JavaScript::set_window_title(const String &p_title) { - if (main_loop) - main_loop->notification(MainLoop::NOTIFICATION_WM_QUIT_REQUEST); + /* clang-format off */ + EM_ASM_({ + document.title = UTF8ToString($0); + }, p_title.utf8().get_data()); + /* clang-format on */ +} + +String OS_JavaScript::get_executable_path() const { + + return OS::get_executable_path(); } Error OS_JavaScript::shell_open(String p_uri) { + + // Open URI in a new tab, browser will deal with it by protocol. /* clang-format off */ EM_ASM_({ window.open(UTF8ToString($0), '_blank'); @@ -850,86 +881,24 @@ Error OS_JavaScript::shell_open(String p_uri) { return OK; } -String OS_JavaScript::get_resource_dir() const { +String OS_JavaScript::get_name() { - return "/"; //javascript has it's own filesystem for resources inside the APK + return "HTML5"; +} + +bool OS_JavaScript::can_draw() const { + + return true; // Always? } String OS_JavaScript::get_user_data_dir() const { - /* - if (get_user_data_dir_func) - return get_user_data_dir_func(); - */ return "/userfs"; }; -String OS_JavaScript::get_executable_path() const { +String OS_JavaScript::get_resource_dir() const { - return OS::get_executable_path(); -} - -void OS_JavaScript::_close_notification_funcs(const String &p_file, int p_flags) { - - OS_JavaScript *os = static_cast(get_singleton()); - if (os->idbfs_available && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) { - os->last_sync_time = OS::get_singleton()->get_ticks_msec(); - os->time_to_save_sync = 5000; //five seconds since last save - } -} - -void OS_JavaScript::process_joypads() { - - int joy_count = emscripten_get_num_gamepads(); - for (int i = 0; i < joy_count; i++) { - EmscriptenGamepadEvent state; - emscripten_get_gamepad_status(i, &state); - if (state.connected) { - - int num_buttons = MIN(state.numButtons, 18); - int num_axes = MIN(state.numAxes, 8); - for (int j = 0; j < num_buttons; j++) { - - float value = state.analogButton[j]; - if (String(state.mapping) == "standard" && (j == 6 || j == 7)) { - InputDefault::JoyAxis jx; - jx.min = 0; - jx.value = value; - input->joy_axis(i, j, jx); - } else { - input->joy_button(i, j, value); - } - } - for (int j = 0; j < num_axes; j++) { - - InputDefault::JoyAxis jx; - jx.min = -1; - jx.value = state.axis[j]; - input->joy_axis(i, j, jx); - } - } - } -} - -bool OS_JavaScript::joy_connection_changed(int p_type, const EmscriptenGamepadEvent *p_event) { - if (p_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) { - - String guid = ""; - if (String(p_event->mapping) == "standard") - guid = "Default HTML5 Gamepad"; - input->joy_connection_changed(p_event->index, true, String(p_event->id), guid); - } else { - input->joy_connection_changed(p_event->index, false, ""); - } - return true; -} - -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); + return "/"; } OS::PowerState OS_JavaScript::get_power_state() { @@ -950,59 +919,53 @@ int OS_JavaScript::get_power_percent_left() { return -1; } -bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { +void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { - if (p_feature == "HTML5" || p_feature == "web") - return true; - -#ifdef JAVASCRIPT_EVAL_ENABLED - if (p_feature == "JavaScript") - return true; -#endif - - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_get_current_context(); - // all extensions are already automatically enabled, this function allows - // checking WebGL extension support without inline JavaScript - if (p_feature == "s3tc" && emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_s3tc_srgb")) - return true; - if (p_feature == "etc" && emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_etc1")) - return true; - if (p_feature == "etc2" && emscripten_webgl_enable_extension(ctx, "WEBGL_compressed_texture_etc")) - return true; - - return false; + OS_JavaScript *os = get_singleton(); + if (os->is_userfs_persistent() && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) { + os->last_sync_check_time = OS::get_singleton()->get_ticks_msec(); + // Wait five seconds in case more files are about to be closed. + os->sync_wait_time = 5000; + } } -void OS_JavaScript::set_idbfs_available(bool p_idbfs_available) { +void OS_JavaScript::set_idb_available(bool p_idb_available) { - idbfs_available = p_idbfs_available; + idb_available = p_idb_available; } bool OS_JavaScript::is_userfs_persistent() const { - return idbfs_available; + return idb_available; } -OS_JavaScript::OS_JavaScript(const char *p_execpath, GetUserDataDirFunc p_get_user_data_dir_func) { +OS_JavaScript *OS_JavaScript::get_singleton() { + + return static_cast(OS::get_singleton()); +} + +OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { + + List arguments; + for (int i = 1; i < p_argc; i++) { + arguments.push_back(String::utf8(p_argv[i])); + } + set_cmdline(p_argv[0], arguments); - set_cmdline(p_execpath, get_cmdline_args()); - main_loop = NULL; window_maximized = false; - soft_fs_enabled = false; + soft_fullscreen_enabled = false; canvas_size_adjustment_requested = false; - get_user_data_dir_func = p_get_user_data_dir_func; - FileAccessUnix::close_notification_func = _close_notification_funcs; + main_loop = NULL; - idbfs_available = false; - time_to_save_sync = -1; + idb_available = false; + sync_wait_time = -1; + + AudioDriverManager::add_driver(&audio_driver_javascript); Vector loggers; loggers.push_back(memnew(StdLogger)); _set_logger(memnew(CompositeLogger(loggers))); - AudioDriverManager::add_driver(&audio_driver_javascript); -} - -OS_JavaScript::~OS_JavaScript() { + FileAccessUnix::close_notification_func = file_access_close_callback; } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 46eb1b3f139..503c92585b7 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -32,52 +32,56 @@ #define OS_JAVASCRIPT_H #include "audio_driver_javascript.h" -#include "drivers/unix/os_unix.h" #include "main/input_default.h" -#include "os/input.h" -#include "os/main_loop.h" #include "servers/audio_server.h" #include "servers/visual/rasterizer.h" +#include "unix/os_unix.h" #include -typedef String (*GetUserDataDirFunc)(); - class OS_JavaScript : public OS_Unix { - bool idbfs_available; - int64_t time_to_save_sync; - int64_t last_sync_time; - - VisualServer *visual_server; - AudioDriverJavaScript audio_driver_javascript; - - InputDefault *input; + VideoMode video_mode; Vector2 windowed_size; bool window_maximized; - bool soft_fs_enabled; + bool soft_fullscreen_enabled; bool canvas_size_adjustment_requested; - VideoMode video_mode; + + InputDefault *input; + Ref deferred_key_event; CursorShape cursor_shape; + Point2 touches[32]; + MainLoop *main_loop; + AudioDriverJavaScript audio_driver_javascript; - GetUserDataDirFunc get_user_data_dir_func; + bool idb_available; + int64_t sync_wait_time; + int64_t last_sync_check_time; - static void _close_notification_funcs(const String &p_file, int p_flags); + static EM_BOOL browser_resize_callback(int p_event_type, const EmscriptenUiEvent *p_event, void *p_user_data); + 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(); - void set_css_cursor(const char *); - const char *get_css_cursor() const; + static void main_loop_callback(); -public: - // functions used by main to initialize/deintialize the OS - 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; + static void file_access_close_callback(const String &p_file, int p_flags); +protected: virtual void initialize_core(); virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver); @@ -86,77 +90,64 @@ public: virtual void finalize(); - typedef int64_t ProcessID; + virtual bool _check_internal_feature_support(const String &p_feature); - //static OS* get_singleton(); - - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - - virtual void set_mouse_mode(MouseMode p_mode); - virtual MouseMode get_mouse_mode() const; - virtual Point2 get_mouse_position() const; - virtual int get_mouse_button_state() const; - virtual void set_window_title(const String &p_title); - - //virtual void set_clipboard(const String& p_text); - //virtual String get_clipboard() const; +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 *p_list, int p_screen = 0) const; - virtual Size2 get_screen_size(int p_screen = -1) 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 { return window_maximized; } - virtual void set_window_fullscreen(bool p_enable); + 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; - void request_canvas_size_adjustment(); - - virtual String get_name(); - virtual MainLoop *get_main_loop() const; - - virtual bool can_draw() const; - - virtual bool is_userfs_persistent() 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); - - void main_loop_begin(); - bool main_loop_iterate(); - void main_loop_request_quit(); - void main_loop_end(); - void main_loop_focusout(); - void main_loop_focusin(); + virtual void set_mouse_mode(MouseMode p_mode); + virtual MouseMode get_mouse_mode() const; virtual bool has_touchscreen_ui_hint() const; - virtual Error shell_open(String p_uri); - virtual String get_user_data_dir() const; - String get_executable_path() const; - virtual String get_resource_dir() const; - - void process_accelerometer(const Vector3 &p_accelerometer); - void push_input(const Ref &p_ev); - virtual bool is_joy_known(int p_device); virtual String get_joy_guid(int p_device) const; - bool joy_connection_changed(int p_type, const EmscriptenGamepadEvent *p_event); + + 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 MainLoop *get_main_loop() const; + void run_async(); + bool main_loop_iterate(); + + virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); + virtual void set_window_title(const String &p_title); + String get_executable_path() const; + virtual Error shell_open(String p_uri); + virtual String get_name(); + virtual bool can_draw() const; + + virtual String get_resource_dir() const; + virtual String get_user_data_dir() const; virtual OS::PowerState get_power_state(); virtual int get_power_seconds_left(); virtual int get_power_percent_left(); - virtual bool _check_internal_feature_support(const String &p_feature); + void set_idb_available(bool p_idb_available); + virtual bool is_userfs_persistent() const; - void set_idbfs_available(bool p_idbfs_available); - - OS_JavaScript(const char *p_execpath, GetUserDataDirFunc p_get_user_data_dir_func); - ~OS_JavaScript(); + OS_JavaScript(int p_argc, char *p_argv[]); }; #endif