diff --git a/platform/javascript/.eslintrc.libs.js b/platform/javascript/.eslintrc.libs.js index 81b1b8c86462..8e579fd46256 100644 --- a/platform/javascript/.eslintrc.libs.js +++ b/platform/javascript/.eslintrc.libs.js @@ -15,6 +15,7 @@ module.exports = { "IDBFS": true, "GodotOS": true, "GodotConfig": true, + "GodotEventListeners": true, "GodotRuntime": true, "GodotFS": true, "IDHandler": true, diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 4403a1ebd9e4..0385ee1ec6eb 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -19,6 +19,7 @@ sys_env.AddJSLibraries( "js/libs/library_godot_fetch.js", "js/libs/library_godot_os.js", "js/libs/library_godot_runtime.js", + "js/libs/library_godot_input.js", ] ) diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h index d332af2c311d..8051b270e623 100644 --- a/platform/javascript/godot_js.h +++ b/platform/javascript/godot_js.h @@ -50,12 +50,28 @@ extern int godot_js_os_execute(const char *p_json); extern void godot_js_os_shell_open(const char *p_uri); extern int godot_js_os_hw_concurrency_get(); +// Input +extern void godot_js_input_mouse_button_cb(int (*p_callback)(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers)); +extern void godot_js_input_mouse_move_cb(void (*p_callback)(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers)); +extern void godot_js_input_mouse_wheel_cb(int (*p_callback)(double p_delta_x, double p_delta_y)); +extern void godot_js_input_touch_cb(void (*p_callback)(int p_type, int p_count), uint32_t *r_identifiers, double *r_coords); +extern void godot_js_input_key_cb(void (*p_callback)(int p_type, int p_repeat, int p_modifiers), char r_code[32], char r_key[32]); + +// Input gamepad +extern void godot_js_input_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); +extern int godot_js_input_gamepad_sample(); +extern int godot_js_input_gamepad_sample_count(); +extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); +extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); +extern void godot_js_input_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); + // Display extern int godot_js_display_screen_dpi_get(); extern double godot_js_display_pixel_ratio_get(); extern void godot_js_display_alert(const char *p_text); extern int godot_js_display_touchscreen_is_available(); extern int godot_js_display_is_swap_ok_cancel(); +extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); // Display canvas extern void godot_js_display_canvas_focus(); @@ -68,7 +84,6 @@ extern void godot_js_display_window_size_get(int32_t *p_x, int32_t *p_y); extern void godot_js_display_screen_size_get(int32_t *p_x, int32_t *p_y); extern int godot_js_display_fullscreen_request(); extern int godot_js_display_fullscreen_exit(); -extern void godot_js_display_compute_position(int p_x, int p_y, int32_t *r_x, int32_t *r_y); extern void godot_js_display_window_title_set(const char *p_text); extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); extern int godot_js_display_has_webgl(int p_version); @@ -82,18 +97,13 @@ extern void godot_js_display_cursor_set_shape(const char *p_cursor); extern int godot_js_display_cursor_is_hidden(); extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); extern void godot_js_display_cursor_set_visible(int p_visible); - -// Display gamepad -extern void godot_js_display_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); -extern int godot_js_display_gamepad_sample(); -extern int godot_js_display_gamepad_sample_count(); -extern int godot_js_display_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); +extern void godot_js_display_cursor_lock_set(int p_lock); +extern int godot_js_display_cursor_is_locked(); // Display listeners +extern void godot_js_display_fullscreen_cb(void (*p_callback)(int p_fullscreen)); +extern void godot_js_display_window_blur_cb(void (*p_callback)()); extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); -extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); -extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); -extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); // Display Virtual Keyboard extern int godot_js_display_vk_available(); diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index affae9037056..2689f1c22c67 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -28,224 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -/* - * Display Server listeners. - * Keeps track of registered event listeners so it can remove them on shutdown. - */ -const GodotDisplayListeners = { - $GodotDisplayListeners__deps: ['$GodotOS'], - $GodotDisplayListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayListeners.clear(); resolve(); });', - $GodotDisplayListeners: { - handlers: [], - - has: function (target, event, method, capture) { - return GodotDisplayListeners.handlers.findIndex(function (e) { - return e.target === target && e.event === event && e.method === method && e.capture === capture; - }) !== -1; - }, - - add: function (target, event, method, capture) { - if (GodotDisplayListeners.has(target, event, method, capture)) { - return; - } - function Handler(p_target, p_event, p_method, p_capture) { - this.target = p_target; - this.event = p_event; - this.method = p_method; - this.capture = p_capture; - } - GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture)); - target.addEventListener(event, method, capture); - }, - - clear: function () { - GodotDisplayListeners.handlers.forEach(function (h) { - h.target.removeEventListener(h.event, h.method, h.capture); - }); - GodotDisplayListeners.handlers.length = 0; - }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayListeners); - -/* - * Drag and drop handler. - * This is pretty big, but basically detect dropped files on GodotConfig.canvas, - * process them one by one (recursively for directories), and copies them to - * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot - * event (that requires a string array of paths). - * - * NOTE: The temporary files are removed after the callback. This means that - * deferred callbacks won't be able to access the files. - */ -const GodotDisplayDragDrop = { - $GodotDisplayDragDrop__deps: ['$FS', '$GodotFS'], - $GodotDisplayDragDrop: { - promises: [], - pending_files: [], - - add_entry: function (entry) { - if (entry.isDirectory) { - GodotDisplayDragDrop.add_dir(entry); - } else if (entry.isFile) { - GodotDisplayDragDrop.add_file(entry); - } else { - GodotRuntime.error('Unrecognized entry...', entry); - } - }, - - add_dir: function (entry) { - GodotDisplayDragDrop.promises.push(new Promise(function (resolve, reject) { - const reader = entry.createReader(); - reader.readEntries(function (entries) { - for (let i = 0; i < entries.length; i++) { - GodotDisplayDragDrop.add_entry(entries[i]); - } - resolve(); - }); - })); - }, - - add_file: function (entry) { - GodotDisplayDragDrop.promises.push(new Promise(function (resolve, reject) { - entry.file(function (file) { - const reader = new FileReader(); - reader.onload = function () { - const 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']; - } - GodotDisplayDragDrop.pending_files.push(f); - resolve(); - }; - reader.onerror = function () { - GodotRuntime.print('Error reading file'); - reject(); - }; - reader.readAsArrayBuffer(file); - }, function (err) { - GodotRuntime.print('Error!'); - reject(); - }); - })); - }, - - process: function (resolve, reject) { - if (GodotDisplayDragDrop.promises.length === 0) { - resolve(); - return; - } - GodotDisplayDragDrop.promises.pop().then(function () { - setTimeout(function () { - GodotDisplayDragDrop.process(resolve, reject); - }, 0); - }); - }, - - _process_event: function (ev, callback) { - ev.preventDefault(); - if (ev.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (let i = 0; i < ev.dataTransfer.items.length; i++) { - const item = ev.dataTransfer.items[i]; - let entry = null; - if ('getAsEntry' in item) { - entry = item.getAsEntry(); - } else if ('webkitGetAsEntry' in item) { - entry = item.webkitGetAsEntry(); - } - if (entry) { - GodotDisplayDragDrop.add_entry(entry); - } - } - } else { - GodotRuntime.error('File upload not supported'); - } - new Promise(GodotDisplayDragDrop.process).then(function () { - const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`; - const drops = []; - const files = []; - FS.mkdir(DROP); - GodotDisplayDragDrop.pending_files.forEach((elem) => { - const path = elem['path']; - GodotFS.copy_to_fs(DROP + path, elem['data']); - let idx = path.indexOf('/'); - if (idx === -1) { - // Root file - drops.push(DROP + path); - } else { - // Subdir - const sub = path.substr(0, idx); - idx = sub.indexOf('/'); - if (idx < 0 && drops.indexOf(DROP + sub) === -1) { - drops.push(DROP + sub); - } - } - files.push(DROP + path); - }); - GodotDisplayDragDrop.promises = []; - GodotDisplayDragDrop.pending_files = []; - callback(drops); - if (GodotConfig.persistent_drops) { - // Delay removal at exit. - GodotOS.atexit(function (resolve, reject) { - GodotDisplayDragDrop.remove_drop(files, DROP); - resolve(); - }); - } else { - GodotDisplayDragDrop.remove_drop(files, DROP); - } - }); - }, - - remove_drop: function (files, drop_path) { - const dirs = [drop_path.substr(0, drop_path.length - 1)]; - // Remove temporary files - files.forEach(function (file) { - FS.unlink(file); - let dir = file.replace(drop_path, ''); - let idx = dir.lastIndexOf('/'); - while (idx > 0) { - dir = dir.substr(0, idx); - if (dirs.indexOf(drop_path + dir) === -1) { - dirs.push(drop_path + dir); - } - idx = dir.lastIndexOf('/'); - } - }); - // Remove dirs. - dirs.sort(function (a, b) { - const al = (a.match(/\//g) || []).length; - const bl = (b.match(/\//g) || []).length; - if (al > bl) { - return -1; - } else if (al < bl) { - return 1; - } - return 0; - }).forEach(function (dir) { - FS.rmdir(dir); - }); - }, - - handler: function (callback) { - return function (ev) { - GodotDisplayDragDrop._process_event(ev, callback); - }; - }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayDragDrop); - const GodotDisplayVK = { - $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotDisplayListeners'], + $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners'], $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });', $GodotDisplayVK: { textinput: null, @@ -271,12 +56,12 @@ const GodotDisplayVK = { elem.style.outline = 'none'; elem.readonly = true; elem.disabled = true; - GodotDisplayListeners.add(elem, 'input', function (evt) { + GodotEventListeners.add(elem, 'input', function (evt) { const c_str = GodotRuntime.allocString(elem.value); input_cb(c_str, elem.selectionEnd); GodotRuntime.free(c_str); }, false); - GodotDisplayListeners.add(elem, 'blur', function (evt) { + GodotEventListeners.add(elem, 'blur', function (evt) { elem.style.display = 'none'; elem.readonly = true; elem.disabled = true; @@ -376,136 +161,23 @@ const GodotDisplayCursor = { delete GodotDisplayCursor.cursors[key]; }); }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayCursor); - -/* - * Display Gamepad API helper. - */ -const GodotDisplayGamepads = { - $GodotDisplayGamepads__deps: ['$GodotRuntime', '$GodotDisplayListeners'], - $GodotDisplayGamepads: { - samples: [], - - get_pads: function () { - try { - // Will throw in iframe when permission is denied. - // Will throw/warn in the future for insecure contexts. - // See https://github.com/w3c/gamepad/pull/120 - const pads = navigator.getGamepads(); - if (pads) { - return pads; - } - return []; - } catch (e) { - return []; - } - }, - - get_samples: function () { - return GodotDisplayGamepads.samples; - }, - - get_sample: function (index) { - const samples = GodotDisplayGamepads.samples; - return index < samples.length ? samples[index] : null; - }, - - sample: function () { - const pads = GodotDisplayGamepads.get_pads(); - const samples = []; - for (let i = 0; i < pads.length; i++) { - const pad = pads[i]; - if (!pad) { - samples.push(null); - continue; - } - const s = { - standard: pad.mapping === 'standard', - buttons: [], - axes: [], - connected: pad.connected, - }; - for (let b = 0; b < pad.buttons.length; b++) { - s.buttons.push(pad.buttons[b].value); - } - for (let a = 0; a < pad.axes.length; a++) { - s.axes.push(pad.axes[a]); - } - samples.push(s); + lockPointer: function () { + const canvas = GodotConfig.canvas; + if (canvas.requestPointerLock) { + canvas.requestPointerLock(); } - GodotDisplayGamepads.samples = samples; }, - - init: function (onchange) { - GodotDisplayListeners.samples = []; - function add(pad) { - const guid = GodotDisplayGamepads.get_guid(pad); - const c_id = GodotRuntime.allocString(pad.id); - const c_guid = GodotRuntime.allocString(guid); - onchange(pad.index, 1, c_id, c_guid); - GodotRuntime.free(c_id); - GodotRuntime.free(c_guid); + releasePointer: function () { + if (document.exitPointerLock) { + document.exitPointerLock(); } - const pads = GodotDisplayGamepads.get_pads(); - for (let i = 0; i < pads.length; i++) { - // Might be reserved space. - if (pads[i]) { - add(pads[i]); - } - } - GodotDisplayListeners.add(window, 'gamepadconnected', function (evt) { - add(evt.gamepad); - }, false); - GodotDisplayListeners.add(window, 'gamepaddisconnected', function (evt) { - onchange(evt.gamepad.index, 0); - }, false); }, - - get_guid: function (pad) { - if (pad.mapping) { - return pad.mapping; - } - const ua = navigator.userAgent; - let os = 'Unknown'; - if (ua.indexOf('Android') >= 0) { - os = 'Android'; - } else if (ua.indexOf('Linux') >= 0) { - os = 'Linux'; - } else if (ua.indexOf('iPhone') >= 0) { - os = 'iOS'; - } else if (ua.indexOf('Macintosh') >= 0) { - // Updated iPads will fall into this category. - os = 'MacOSX'; - } else if (ua.indexOf('Windows') >= 0) { - os = 'Windows'; - } - - const id = pad.id; - // Chrom* style: NAME (Vendor: xxxx Product: xxxx) - const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; - // Firefox/Safari style (safari may remove leading zeores) - const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; - let vendor = ''; - let product = ''; - if (exp1.test(id)) { - const match = exp1.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } else if (exp2.test(id)) { - const match = exp2.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } - if (!vendor || !product) { - return `${os}Unknown`; - } - return os + vendor + product; + isPointerLocked: function () { + return document.pointerLockElement === GodotConfig.canvas; }, }, }; -mergeInto(LibraryManager.library, GodotDisplayGamepads); +mergeInto(LibraryManager.library, GodotDisplayCursor); const GodotDisplayScreen = { $GodotDisplayScreen__deps: ['$GodotConfig', '$GodotOS', '$GL', 'emscripten_webgl_get_current_context'], @@ -622,7 +294,7 @@ mergeInto(LibraryManager.library, GodotDisplayScreen); * Exposes all the functions needed by DisplayServer implementation. */ const GodotDisplay = { - $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads', '$GodotDisplayScreen', '$GodotDisplayVK'], + $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'], $GodotDisplay: { window_icon: '', findDPI: function () { @@ -710,15 +382,6 @@ const GodotDisplay = { GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32'); }, - godot_js_display_compute_position: function (x, y, r_x, r_y) { - const canvas = GodotConfig.canvas; - const rect = canvas.getBoundingClientRect(); - const rw = canvas.width / rect.width; - const rh = canvas.height / rect.height; - GodotRuntime.setHeapValue(r_x, (x - rect.x) * rw, 'i32'); - GodotRuntime.setHeapValue(r_y, (y - rect.y) * rh, 'i32'); - }, - godot_js_display_has_webgl__sig: 'ii', godot_js_display_has_webgl: function (p_version) { if (p_version !== 1 && p_version !== 2) { @@ -859,60 +522,64 @@ const GodotDisplay = { } }, + godot_js_display_cursor_lock_set__sig: 'vi', + godot_js_display_cursor_lock_set: function (p_lock) { + if (p_lock) { + GodotDisplayCursor.lockPointer(); + } else { + GodotDisplayCursor.releasePointer(); + } + }, + + godot_js_display_cursor_is_locked__sig: 'i', + godot_js_display_cursor_is_locked: function () { + return GodotDisplayCursor.isPointerLocked() ? 1 : 0; + }, + /* * Listeners */ - godot_js_display_notification_cb__sig: 'viiiii', - godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { + godot_js_display_fullscreen_cb__sig: 'vi', + godot_js_display_fullscreen_cb: function (callback) { const canvas = GodotConfig.canvas; const func = GodotRuntime.get_func(callback); - const notif = [p_enter, p_exit, p_in, p_out]; - ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { - GodotDisplayListeners.add(canvas, evt_name, function () { - func(notif[idx]); - }, true); - }); + function change_cb(evt) { + if (evt.target === canvas) { + func(GodotDisplayScreen.isFullscreen()); + } + } + GodotEventListeners.add(document, 'fullscreenchange', change_cb, false); + GodotEventListeners.add(document, 'mozfullscreenchange', change_cb, false); + GodotEventListeners.add(document, 'webkitfullscreenchange', change_cb, false); }, - godot_js_display_paste_cb__sig: 'vi', - godot_js_display_paste_cb: function (callback) { + godot_js_display_window_blur_cb__sig: 'vi', + godot_js_display_window_blur_cb: function (callback) { const func = GodotRuntime.get_func(callback); - GodotDisplayListeners.add(window, 'paste', function (evt) { - const text = evt.clipboardData.getData('text'); - const ptr = GodotRuntime.allocString(text); - func(ptr); - GodotRuntime.free(ptr); + GodotEventListeners.add(window, 'blur', function () { + func(); }, false); }, - godot_js_display_drop_files_cb__sig: 'vi', - godot_js_display_drop_files_cb: function (callback) { - const func = GodotRuntime.get_func(callback); - const dropFiles = function (files) { - const args = files || []; - if (!args.length) { - return; - } - const argc = args.length; - const argv = GodotRuntime.allocStringArray(args); - func(argv, argc); - GodotRuntime.freeStringArray(argv, argc); - }; + godot_js_display_notification_cb__sig: 'viiiii', + godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { const canvas = GodotConfig.canvas; - GodotDisplayListeners.add(canvas, 'dragover', function (ev) { - // Prevent default behavior (which would try to open the file(s)) - ev.preventDefault(); - }, false); - GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles)); + const func = GodotRuntime.get_func(callback); + const notif = [p_enter, p_exit, p_in, p_out]; + ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { + GodotEventListeners.add(canvas, evt_name, function () { + func(notif[idx]); + }, true); + }); }, godot_js_display_setup_canvas__sig: 'viiii', godot_js_display_setup_canvas: function (p_width, p_height, p_fullscreen, p_hidpi) { const canvas = GodotConfig.canvas; - GodotDisplayListeners.add(canvas, 'contextmenu', function (ev) { + GodotEventListeners.add(canvas, 'contextmenu', function (ev) { ev.preventDefault(); }, false); - GodotDisplayListeners.add(canvas, 'webglcontextlost', function (ev) { + GodotEventListeners.add(canvas, 'webglcontextlost', function (ev) { alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert ev.preventDefault(); }, false); @@ -965,49 +632,6 @@ const GodotDisplay = { GodotDisplayVK.init(input_cb); } }, - - /* - * Gamepads - */ - godot_js_display_gamepad_cb__sig: 'vi', - godot_js_display_gamepad_cb: function (change_cb) { - const onchange = GodotRuntime.get_func(change_cb); - GodotDisplayGamepads.init(onchange); - }, - - godot_js_display_gamepad_sample_count__sig: 'i', - godot_js_display_gamepad_sample_count: function () { - return GodotDisplayGamepads.get_samples().length; - }, - - godot_js_display_gamepad_sample__sig: 'i', - godot_js_display_gamepad_sample: function () { - GodotDisplayGamepads.sample(); - return 0; - }, - - godot_js_display_gamepad_sample_get__sig: 'iiiiiii', - godot_js_display_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { - const sample = GodotDisplayGamepads.get_sample(p_index); - if (!sample || !sample.connected) { - return 1; - } - const btns = sample.buttons; - const btns_len = btns.length < 16 ? btns.length : 16; - for (let i = 0; i < btns_len; i++) { - GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); - } - GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); - const axes = sample.axes; - const axes_len = axes.length < 10 ? axes.length : 10; - for (let i = 0; i < axes_len; i++) { - GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); - } - GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); - const is_standard = sample.standard ? 1 : 0; - GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); - return 0; - }, }; autoAddDeps(GodotDisplay, '$GodotDisplay'); diff --git a/platform/javascript/js/libs/library_godot_input.js b/platform/javascript/js/libs/library_godot_input.js new file mode 100644 index 000000000000..587c320f3551 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_input.js @@ -0,0 +1,526 @@ +/*************************************************************************/ +/* library_godot_input.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +/* + * Gamepad API helper. + */ +const GodotInputGamepads = { + $GodotInputGamepads__deps: ['$GodotRuntime', '$GodotEventListeners'], + $GodotInputGamepads: { + samples: [], + + get_pads: function () { + try { + // Will throw in iframe when permission is denied. + // Will throw/warn in the future for insecure contexts. + // See https://github.com/w3c/gamepad/pull/120 + const pads = navigator.getGamepads(); + if (pads) { + return pads; + } + return []; + } catch (e) { + return []; + } + }, + + get_samples: function () { + return GodotInputGamepads.samples; + }, + + get_sample: function (index) { + const samples = GodotInputGamepads.samples; + return index < samples.length ? samples[index] : null; + }, + + sample: function () { + const pads = GodotInputGamepads.get_pads(); + const samples = []; + for (let i = 0; i < pads.length; i++) { + const pad = pads[i]; + if (!pad) { + samples.push(null); + continue; + } + const s = { + standard: pad.mapping === 'standard', + buttons: [], + axes: [], + connected: pad.connected, + }; + for (let b = 0; b < pad.buttons.length; b++) { + s.buttons.push(pad.buttons[b].value); + } + for (let a = 0; a < pad.axes.length; a++) { + s.axes.push(pad.axes[a]); + } + samples.push(s); + } + GodotInputGamepads.samples = samples; + }, + + init: function (onchange) { + GodotEventListeners.samples = []; + function add(pad) { + const guid = GodotInputGamepads.get_guid(pad); + const c_id = GodotRuntime.allocString(pad.id); + const c_guid = GodotRuntime.allocString(guid); + onchange(pad.index, 1, c_id, c_guid); + GodotRuntime.free(c_id); + GodotRuntime.free(c_guid); + } + const pads = GodotInputGamepads.get_pads(); + for (let i = 0; i < pads.length; i++) { + // Might be reserved space. + if (pads[i]) { + add(pads[i]); + } + } + GodotEventListeners.add(window, 'gamepadconnected', function (evt) { + add(evt.gamepad); + }, false); + GodotEventListeners.add(window, 'gamepaddisconnected', function (evt) { + onchange(evt.gamepad.index, 0); + }, false); + }, + + get_guid: function (pad) { + if (pad.mapping) { + return pad.mapping; + } + const ua = navigator.userAgent; + let os = 'Unknown'; + if (ua.indexOf('Android') >= 0) { + os = 'Android'; + } else if (ua.indexOf('Linux') >= 0) { + os = 'Linux'; + } else if (ua.indexOf('iPhone') >= 0) { + os = 'iOS'; + } else if (ua.indexOf('Macintosh') >= 0) { + // Updated iPads will fall into this category. + os = 'MacOSX'; + } else if (ua.indexOf('Windows') >= 0) { + os = 'Windows'; + } + + const id = pad.id; + // Chrom* style: NAME (Vendor: xxxx Product: xxxx) + const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; + // Firefox/Safari style (safari may remove leading zeores) + const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; + let vendor = ''; + let product = ''; + if (exp1.test(id)) { + const match = exp1.exec(id); + vendor = match[1].padStart(4, '0'); + product = match[2].padStart(4, '0'); + } else if (exp2.test(id)) { + const match = exp2.exec(id); + vendor = match[1].padStart(4, '0'); + product = match[2].padStart(4, '0'); + } + if (!vendor || !product) { + return `${os}Unknown`; + } + return os + vendor + product; + }, + }, +}; +mergeInto(LibraryManager.library, GodotInputGamepads); + +/* + * Drag and drop helper. + * This is pretty big, but basically detect dropped files on GodotConfig.canvas, + * process them one by one (recursively for directories), and copies them to + * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot + * event (that requires a string array of paths). + * + * NOTE: The temporary files are removed after the callback. This means that + * deferred callbacks won't be able to access the files. + */ +const GodotInputDragDrop = { + $GodotInputDragDrop__deps: ['$FS', '$GodotFS'], + $GodotInputDragDrop: { + promises: [], + pending_files: [], + + add_entry: function (entry) { + if (entry.isDirectory) { + GodotInputDragDrop.add_dir(entry); + } else if (entry.isFile) { + GodotInputDragDrop.add_file(entry); + } else { + GodotRuntime.error('Unrecognized entry...', entry); + } + }, + + add_dir: function (entry) { + GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { + const reader = entry.createReader(); + reader.readEntries(function (entries) { + for (let i = 0; i < entries.length; i++) { + GodotInputDragDrop.add_entry(entries[i]); + } + resolve(); + }); + })); + }, + + add_file: function (entry) { + GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { + entry.file(function (file) { + const reader = new FileReader(); + reader.onload = function () { + const 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']; + } + GodotInputDragDrop.pending_files.push(f); + resolve(); + }; + reader.onerror = function () { + GodotRuntime.print('Error reading file'); + reject(); + }; + reader.readAsArrayBuffer(file); + }, function (err) { + GodotRuntime.print('Error!'); + reject(); + }); + })); + }, + + process: function (resolve, reject) { + if (GodotInputDragDrop.promises.length === 0) { + resolve(); + return; + } + GodotInputDragDrop.promises.pop().then(function () { + setTimeout(function () { + GodotInputDragDrop.process(resolve, reject); + }, 0); + }); + }, + + _process_event: function (ev, callback) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + let entry = null; + if ('getAsEntry' in item) { + entry = item.getAsEntry(); + } else if ('webkitGetAsEntry' in item) { + entry = item.webkitGetAsEntry(); + } + if (entry) { + GodotInputDragDrop.add_entry(entry); + } + } + } else { + GodotRuntime.error('File upload not supported'); + } + new Promise(GodotInputDragDrop.process).then(function () { + const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`; + const drops = []; + const files = []; + FS.mkdir(DROP); + GodotInputDragDrop.pending_files.forEach((elem) => { + const path = elem['path']; + GodotFS.copy_to_fs(DROP + path, elem['data']); + let idx = path.indexOf('/'); + if (idx === -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + const sub = path.substr(0, idx); + idx = sub.indexOf('/'); + if (idx < 0 && drops.indexOf(DROP + sub) === -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + GodotInputDragDrop.promises = []; + GodotInputDragDrop.pending_files = []; + callback(drops); + if (GodotConfig.persistent_drops) { + // Delay removal at exit. + GodotOS.atexit(function (resolve, reject) { + GodotInputDragDrop.remove_drop(files, DROP); + resolve(); + }); + } else { + GodotInputDragDrop.remove_drop(files, DROP); + } + }); + }, + + remove_drop: function (files, drop_path) { + const dirs = [drop_path.substr(0, drop_path.length - 1)]; + // Remove temporary files + files.forEach(function (file) { + FS.unlink(file); + let dir = file.replace(drop_path, ''); + let idx = dir.lastIndexOf('/'); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(drop_path + dir) === -1) { + dirs.push(drop_path + dir); + } + idx = dir.lastIndexOf('/'); + } + }); + // Remove dirs. + dirs.sort(function (a, b) { + const al = (a.match(/\//g) || []).length; + const bl = (b.match(/\//g) || []).length; + if (al > bl) { + return -1; + } else if (al < bl) { + return 1; + } + return 0; + }).forEach(function (dir) { + FS.rmdir(dir); + }); + }, + + handler: function (callback) { + return function (ev) { + GodotInputDragDrop._process_event(ev, callback); + }; + }, + }, +}; +mergeInto(LibraryManager.library, GodotInputDragDrop); + +/* + * Godot exposed input functions. + */ +const GodotInput = { + $GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop'], + $GodotInput: { + getModifiers: function (evt) { + return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3); + }, + computePosition: function (evt, rect) { + const canvas = GodotConfig.canvas; + const rw = canvas.width / rect.width; + const rh = canvas.height / rect.height; + const x = (evt.clientX - rect.x) * rw; + const y = (evt.clientY - rect.y) * rh; + return [x, y]; + }, + }, + + /* + * Mouse API + */ + godot_js_input_mouse_move_cb__sig: 'vi', + godot_js_input_mouse_move_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function move_cb(evt) { + const rect = canvas.getBoundingClientRect(); + const pos = GodotInput.computePosition(evt, rect); + // Scale movement + const rw = canvas.width / rect.width; + const rh = canvas.height / rect.height; + const rel_pos_x = evt.movementX * rw; + const rel_pos_y = evt.movementY * rh; + const modifiers = GodotInput.getModifiers(evt); + func(pos[0], pos[1], rel_pos_x, rel_pos_y, modifiers); + } + GodotEventListeners.add(window, 'mousemove', move_cb, false); + }, + + godot_js_input_mouse_wheel_cb__sig: 'vi', + godot_js_input_mouse_wheel_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + function wheel_cb(evt) { + if (func(evt['deltaX'] || 0, evt['deltaY'] || 0)) { + evt.preventDefault(); + } + } + GodotEventListeners.add(GodotConfig.canvas, 'wheel', wheel_cb, false); + }, + + godot_js_input_mouse_button_cb__sig: 'vi', + godot_js_input_mouse_button_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function button_cb(p_pressed, evt) { + const rect = canvas.getBoundingClientRect(); + const pos = GodotInput.computePosition(evt, rect); + const modifiers = GodotInput.getModifiers(evt); + if (func(p_pressed, evt.button, pos[0], pos[1], modifiers)) { + evt.preventDefault(); + } + } + GodotEventListeners.add(canvas, 'mousedown', button_cb.bind(null, 1), false); + GodotEventListeners.add(window, 'mouseup', button_cb.bind(null, 0), false); + }, + + /* + * Touch API + */ + godot_js_input_touch_cb__sig: 'viii', + godot_js_input_touch_cb: function (callback, ids, coords) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function touch_cb(type, evt) { + const rect = canvas.getBoundingClientRect(); + const touches = evt.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + const pos = GodotInput.computePosition(touch, rect); + GodotRuntime.setHeapValue(coords + (i * 2), pos[0], 'double'); + GodotRuntime.setHeapValue(coords + (i * 2 + 8), pos[1], 'double'); + GodotRuntime.setHeapValue(ids + i, touch.identifier, 'i32'); + } + func(type, touches.length); + if (evt.cancelable) { + evt.preventDefault(); + } + } + GodotEventListeners.add(canvas, 'touchstart', touch_cb.bind(null, 0), false); + GodotEventListeners.add(canvas, 'touchend', touch_cb.bind(null, 1), false); + GodotEventListeners.add(canvas, 'touchcancel', touch_cb.bind(null, 1), false); + GodotEventListeners.add(canvas, 'touchmove', touch_cb.bind(null, 2), false); + }, + + /* + * Key API + */ + godot_js_input_key_cb__sig: 'viii', + godot_js_input_key_cb: function (callback, code, key) { + const func = GodotRuntime.get_func(callback); + function key_cb(pressed, evt) { + const modifiers = GodotInput.getModifiers(evt); + GodotRuntime.stringToHeap(evt.code, code, 32); + GodotRuntime.stringToHeap(evt.key, key, 32); + func(pressed, evt.repeat, modifiers); + evt.preventDefault(); + } + GodotEventListeners.add(GodotConfig.canvas, 'keydown', key_cb.bind(null, 1), false); + GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false); + }, + + /* + * Gamepad API + */ + godot_js_input_gamepad_cb__sig: 'vi', + godot_js_input_gamepad_cb: function (change_cb) { + const onchange = GodotRuntime.get_func(change_cb); + GodotInputGamepads.init(onchange); + }, + + godot_js_input_gamepad_sample_count__sig: 'i', + godot_js_input_gamepad_sample_count: function () { + return GodotInputGamepads.get_samples().length; + }, + + godot_js_input_gamepad_sample__sig: 'i', + godot_js_input_gamepad_sample: function () { + GodotInputGamepads.sample(); + return 0; + }, + + godot_js_input_gamepad_sample_get__sig: 'iiiiiii', + godot_js_input_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { + const sample = GodotInputGamepads.get_sample(p_index); + if (!sample || !sample.connected) { + return 1; + } + const btns = sample.buttons; + const btns_len = btns.length < 16 ? btns.length : 16; + for (let i = 0; i < btns_len; i++) { + GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); + } + GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); + const axes = sample.axes; + const axes_len = axes.length < 10 ? axes.length : 10; + for (let i = 0; i < axes_len; i++) { + GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); + } + GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); + const is_standard = sample.standard ? 1 : 0; + GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); + return 0; + }, + + /* + * Drag/Drop API + */ + godot_js_input_drop_files_cb__sig: 'vi', + godot_js_input_drop_files_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const dropFiles = function (files) { + const args = files || []; + if (!args.length) { + return; + } + const argc = args.length; + const argv = GodotRuntime.allocStringArray(args); + func(argv, argc); + GodotRuntime.freeStringArray(argv, argc); + }; + const canvas = GodotConfig.canvas; + GodotEventListeners.add(canvas, 'dragover', function (ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }, false); + GodotEventListeners.add(canvas, 'drop', GodotInputDragDrop.handler(dropFiles)); + }, + + /* Paste API */ + godot_js_input_paste_cb__sig: 'vi', + godot_js_input_paste_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + GodotEventListeners.add(window, 'paste', function (evt) { + const text = evt.clipboardData.getData('text'); + const ptr = GodotRuntime.allocString(text); + func(ptr); + GodotRuntime.free(ptr); + }, false); + }, +}; + +autoAddDeps(GodotInput, '$GodotInput'); +mergeInto(LibraryManager.library, GodotInput); diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 99e7ee8b5f25..c552e99415df 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -328,3 +328,43 @@ const GodotOS = { autoAddDeps(GodotOS, '$GodotOS'); mergeInto(LibraryManager.library, GodotOS); + +/* + * Godot event listeners. + * Keeps track of registered event listeners so it can remove them on shutdown. + */ +const GodotEventListeners = { + $GodotEventListeners__deps: ['$GodotOS'], + $GodotEventListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotEventListeners.clear(); resolve(); });', + $GodotEventListeners: { + handlers: [], + + has: function (target, event, method, capture) { + return GodotEventListeners.handlers.findIndex(function (e) { + return e.target === target && e.event === event && e.method === method && e.capture === capture; + }) !== -1; + }, + + add: function (target, event, method, capture) { + if (GodotEventListeners.has(target, event, method, capture)) { + return; + } + function Handler(p_target, p_event, p_method, p_capture) { + this.target = p_target; + this.event = p_event; + this.method = p_method; + this.capture = p_capture; + } + GodotEventListeners.handlers.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }, + + clear: function () { + GodotEventListeners.handlers.forEach(function (h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + GodotEventListeners.handlers.length = 0; + }, + }, +}; +mergeInto(LibraryManager.library, GodotEventListeners); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 033260cd1fd5..75b9214f7bf9 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -92,38 +92,17 @@ void OS_JavaScript::send_notification_callback(int p_notification) { // Window (canvas) -Point2 OS_JavaScript::compute_position_in_canvas(int p_x, int p_y) { - int point[2]; - godot_js_display_compute_position(p_x, p_y, point, point + 1); - return Point2(point[0], point[1]); -} - bool OS_JavaScript::check_size_force_redraw() { return godot_js_display_size_update() != 0; } -EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { +void OS_JavaScript::fullscreen_change_callback(int p_fullscreen) { OS_JavaScript *os = get_singleton(); - // Empty ID is canvas. - String target_id = String::utf8(p_event->id); - if (target_id.empty() || target_id == String::utf8(&(os->canvas_id[1]))) { - // 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; + os->video_mode.fullscreen = p_fullscreen; } -EM_BOOL OS_JavaScript::blur_callback(int p_event_type, const EmscriptenFocusEvent *p_event, void *p_user_data) { +void OS_JavaScript::window_blur_callback() { get_singleton()->input->release_pressed_events(); - return false; } void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) { @@ -142,15 +121,9 @@ Size2 OS_JavaScript::get_screen_size(int p_screen) const { void OS_JavaScript::set_window_size(const Size2 p_size) { if (video_mode.fullscreen) { - window_maximized = false; set_window_fullscreen(false); - } else { - if (window_maximized) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } - godot_js_display_desired_size_set(p_size.x, p_size.y); } + godot_js_display_desired_size_set(p_size.x, p_size.y); } Size2 OS_JavaScript::get_window_size() const { @@ -160,29 +133,11 @@ Size2 OS_JavaScript::get_window_size() const { } void OS_JavaScript::set_window_maximized(bool p_enabled) { -#ifndef TOOLS_ENABLED - if (video_mode.fullscreen) { - window_maximized = p_enabled; - set_window_fullscreen(false); - } else if (!p_enabled) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } else if (!window_maximized) { - // Prevent calling emscripten_enter_soft_fullscreen mutltiple times, - // this would hide page elements permanently. - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = NULL; - emscripten_enter_soft_fullscreen(canvas_id, &strategy); - window_maximized = p_enabled; - } -#endif + WARN_PRINT_ONCE("Maximizing windows is not supported for the HTML5 platform."); } bool OS_JavaScript::is_window_maximized() const { - return window_maximized; + return false; } void OS_JavaScript::set_window_fullscreen(bool p_enabled) { @@ -193,14 +148,8 @@ void OS_JavaScript::set_window_fullscreen(bool p_enabled) { // Just request changes here, if successful, logic continues in // fullscreen_change_callback. if (p_enabled) { - if (window_maximized) { - // Soft fullsreen during real fullscreen can cause issues, so exit. - // This must be called before requesting full screen. - emscripten_exit_soft_fullscreen(); - } int result = godot_js_display_fullscreen_request(); ERR_FAIL_COND_MSG(result, "The request was denied. Remember that enabling fullscreen is only possible from an input callback for the HTML5 platform."); - entering_fullscreen = true; } else { // No logic allowed here, since exiting w/ ESC key won't use this function. ERR_FAIL_COND(godot_js_display_fullscreen_exit()); @@ -232,76 +181,35 @@ void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) { // Keys -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); +static void dom2godot_mod(Ref ev, int p_mod) { + ev->set_shift(p_mod & 1); + ev->set_alt(p_mod & 2); + ev->set_control(p_mod & 4); + ev->set_metakey(p_mod & 8); } -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(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false)); - ev->set_physical_scancode(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 OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { +void OS_JavaScript::key_callback(int p_pressed, int p_repeat, int p_modifiers) { 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); - - // Make sure to flush all events so we can call restricted APIs inside the event. - os->input->flush_buffered_events(); - + JSKeyEvent &key_event = os->key_event; // Resume audio context after input in case autoplay was denied. os->resume_audio(); - return true; -} - -EM_BOOL OS_JavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - OS_JavaScript *os = get_singleton(); - os->deferred_key_event->set_unicode(p_event->charCode); - os->input->parse_input_event(os->deferred_key_event); - // Make sure to flush all events so we can call restricted APIs inside the event. - os->input->flush_buffered_events(); - - return true; -} + Ref ev; + ev.instance(); + ev->set_echo(p_repeat); + ev->set_scancode(dom_code2godot_scancode(key_event.code, key_event.key, false)); + ev->set_physical_scancode(dom_code2godot_scancode(key_event.code, key_event.key, true)); + ev->set_pressed(p_pressed); + dom2godot_mod(ev, p_modifiers); -EM_BOOL OS_JavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - OS_JavaScript *os = get_singleton(); - Ref ev = setup_key_event(p_event); - ev->set_pressed(false); + String unicode = String::utf8(key_event.key); + if (unicode.length() == 1) { + ev->set_unicode(unicode[0]); + } os->input->parse_input_event(ev); // Make sure to flush all events so we can call restricted APIs inside the event. os->input->flush_buffered_events(); - - return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0; } // Mouse @@ -314,17 +222,18 @@ 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) { +int OS_JavaScript::mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers) { OS_JavaScript *os = get_singleton(); Ref 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_pressed(p_pressed); + ev->set_position(Point2(p_x, p_y)); ev->set_global_position(ev->get_position()); - dom2godot_mod(p_event, ev); + ev->set_pressed(p_pressed); + dom2godot_mod(ev, p_modifiers); - switch (p_event->button) { + switch (p_button) { case DOM_BUTTON_LEFT: ev->set_button_index(BUTTON_LEFT); break; @@ -344,8 +253,8 @@ EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenM return false; } - if (ev->is_pressed()) { - double diff = emscripten_get_now() - os->last_click_ms; + if (p_pressed) { + uint64_t diff = (OS::get_singleton()->get_ticks_usec() / 1000) - os->last_click_ms; if (ev->get_button_index() == os->last_click_button_index) { if (diff < 400 && Point2(os->last_click_pos).distance_to(ev->get_position()) < 5) { @@ -368,9 +277,6 @@ EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenM 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. - godot_js_display_canvas_focus(); mask |= button_flag; } else if (mask & button_flag) { mask &= ~button_flag; @@ -381,43 +287,39 @@ EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenM ev->set_button_mask(mask); os->input->parse_input_event(ev); + // Resume audio context after input in case autoplay was denied. + os->resume_audio(); // Make sure to flush all events so we can call restricted APIs inside the event. os->input->flush_buffered_events(); - // Resume audio context after input in case autoplay was denied. - os->resume_audio(); - // Prevent multi-click text selection and wheel-click scrolling anchor. // Context menu is prevented through contextmenu event. return true; } -EM_BOOL OS_JavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { +void OS_JavaScript::mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers) { OS_JavaScript *os = get_singleton(); int input_mask = os->input->get_mouse_button_mask(); - 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 (!os->cursor_inside_canvas && !input_mask) - return false; + return; Ref ev; ev.instance(); - dom2godot_mod(p_event, ev); + dom2godot_mod(ev, p_modifiers); ev->set_button_mask(input_mask); - ev->set_position(pos); + ev->set_position(Point2(p_x, p_y)); ev->set_global_position(ev->get_position()); - ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); + ev->set_relative(Vector2(p_rel_x, p_rel_y)); os->input->set_mouse_position(ev->get_position()); ev->set_speed(os->input->get_last_mouse_speed()); os->input->parse_input_event(ev); - // Don't suppress mouseover/-leave events. - return false; } static const char *godot2dom_cursor(OS::CursorShape p_shape) { @@ -554,34 +456,33 @@ void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) { if (p_mode == MOUSE_MODE_VISIBLE) { godot_js_display_cursor_set_visible(1); - emscripten_exit_pointerlock(); + godot_js_display_cursor_lock_set(false); } else if (p_mode == MOUSE_MODE_HIDDEN) { godot_js_display_cursor_set_visible(0); - emscripten_exit_pointerlock(); + godot_js_display_cursor_lock_set(false); } else if (p_mode == MOUSE_MODE_CAPTURED) { godot_js_display_cursor_set_visible(1); - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(canvas_id, 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."); + godot_js_display_cursor_lock_set(true); } } OS::MouseMode OS_JavaScript::get_mouse_mode() const { - if (godot_js_display_cursor_is_hidden()) + if (godot_js_display_cursor_is_hidden()) { return MOUSE_MODE_HIDDEN; - - EmscriptenPointerlockChangeEvent ev; - emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == String::utf8(&canvas_id[1])) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; + } + if (godot_js_display_cursor_is_locked()) { + return MOUSE_MODE_CAPTURED; + } + return 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); +int OS_JavaScript::mouse_wheel_callback(double p_delta_x, double p_delta_y) { OS_JavaScript *os = get_singleton(); + if (!godot_js_display_canvas_is_focused()) { if (os->cursor_inside_canvas) { godot_js_display_canvas_focus(); @@ -601,16 +502,17 @@ EM_BOOL OS_JavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEve ev->set_control(input->is_key_pressed(KEY_CONTROL)); ev->set_metakey(input->is_key_pressed(KEY_META)); - if (p_event->deltaY < 0) + if (p_delta_y < 0) { ev->set_button_index(BUTTON_WHEEL_UP); - else if (p_event->deltaY > 0) + } else if (p_delta_y > 0) { ev->set_button_index(BUTTON_WHEEL_DOWN); - else if (p_event->deltaX > 0) + } else if (p_delta_x > 0) { ev->set_button_index(BUTTON_WHEEL_LEFT); - else if (p_event->deltaX < 0) + } else if (p_delta_x < 0) { ev->set_button_index(BUTTON_WHEEL_RIGHT); - else + } else { return false; + } // Different browsers give wildly different delta values, and we can't // interpret deltaMode, so use default value for wheel events' factor. @@ -635,53 +537,41 @@ bool OS_JavaScript::has_touchscreen_ui_hint() const { return godot_js_display_touchscreen_is_available(); } -EM_BOOL OS_JavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { +void OS_JavaScript::touch_callback(int p_type, int p_count) { OS_JavaScript *os = get_singleton(); - Ref ev; - 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.instance(); - 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); - } - - // Make sure to flush all events so we can call restricted APIs inside the event. - os->input->flush_buffered_events(); - // Resume audio context after input in case autoplay was denied. os->resume_audio(); - return true; -} -EM_BOOL OS_JavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - OS_JavaScript *os = get_singleton(); - Ref ev; - 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.instance(); - 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); + const JSTouchEvent &touch_event = os->touch_event; + for (int i = 0; i < p_count; i++) { + Point2 point(touch_event.coords[i * 2], touch_event.coords[i * 2 + 1]); + if (p_type == 2) { + // touchmove + Ref ev; + ev.instance(); + ev->set_index(touch_event.identifier[i]); + ev->set_position(point); + + Point2 &prev = os->touches[i]; + ev->set_relative(ev->get_position() - prev); + prev = ev->get_position(); + + os->input->parse_input_event(ev); + } else { + // touchstart/touchend + Ref ev; + ev.instance(); + ev->set_index(touch_event.identifier[i]); + ev->set_position(point); + ev->set_pressed(p_type == 0); + os->touches[i] = point; + + os->input->parse_input_event(ev); + + // Make sure to flush all events so we can call restricted APIs inside the event. + os->input->flush_buffered_events(); + } } - return true; } // Gamepad @@ -695,14 +585,14 @@ void OS_JavaScript::gamepad_callback(int p_index, int p_connected, const char *p } void OS_JavaScript::process_joypads() { - int32_t pads = godot_js_display_gamepad_sample_count(); + int32_t pads = godot_js_input_gamepad_sample_count(); int32_t s_btns_num = 0; int32_t s_axes_num = 0; int32_t s_standard = 0; float s_btns[16]; float s_axes[10]; for (int idx = 0; idx < pads; idx++) { - int err = godot_js_display_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); + int err = godot_js_input_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); if (err) { continue; } @@ -874,44 +764,24 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, #endif 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, NULL, true, &cb); \ - EM_CHECK(ev) -#define SET_EM_WINDOW_CALLBACK(ev, cb) \ - result = emscripten_set_##ev##_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &cb); \ - EM_CHECK(ev) - // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. - SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) - SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) - SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) - SET_EM_WINDOW_CALLBACK(blur, blur_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) -#undef SET_EM_CALLBACK -#undef EM_CHECK - - // For APIs that are not (sufficiently) exposed, a - // library is used below (implemented in library_godot_display.js). + // JS Input interface (js/libs/library_godot_input.js) + godot_js_input_mouse_button_cb(&OS_JavaScript::mouse_button_callback); + godot_js_input_mouse_move_cb(&OS_JavaScript::mouse_move_callback); + godot_js_input_mouse_wheel_cb(&OS_JavaScript::mouse_wheel_callback); + godot_js_input_touch_cb(&OS_JavaScript::touch_callback, touch_event.identifier, touch_event.coords); + godot_js_input_key_cb(&OS_JavaScript::key_callback, key_event.code, key_event.key); + godot_js_input_gamepad_cb(&OS_JavaScript::gamepad_callback); + godot_js_input_paste_cb(&OS_JavaScript::update_clipboard_callback); + godot_js_input_drop_files_cb(&OS_JavaScript::drop_files_callback); + + // JS Display interface (js/libs/library_godot_display.js) + godot_js_display_fullscreen_cb(&OS_JavaScript::fullscreen_change_callback); + godot_js_display_window_blur_cb(&window_blur_callback); godot_js_display_notification_cb(&OS_JavaScript::send_notification_callback, MainLoop::NOTIFICATION_WM_MOUSE_ENTER, MainLoop::NOTIFICATION_WM_MOUSE_EXIT, MainLoop::NOTIFICATION_WM_FOCUS_IN, MainLoop::NOTIFICATION_WM_FOCUS_OUT); - godot_js_display_paste_cb(&OS_JavaScript::update_clipboard_callback); - godot_js_display_drop_files_cb(&OS_JavaScript::drop_files_callback); - godot_js_display_gamepad_cb(&OS_JavaScript::gamepad_callback); godot_js_display_vk_cb(&input_text_callback); visual_server->init(); @@ -986,22 +856,10 @@ bool OS_JavaScript::main_loop_iterate() { input->flush_buffered_events(); - if (godot_js_display_gamepad_sample() == OK) + if (godot_js_input_gamepad_sample() == OK) { process_joypads(); - - if (just_exited_fullscreen) { - if (window_maximized) { - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = NULL; - emscripten_enter_soft_fullscreen(canvas_id, &strategy); - } else { - godot_js_display_size_update(); - } - just_exited_fullscreen = false; } + return Main::iteration(); } @@ -1218,9 +1076,6 @@ OS_JavaScript::OS_JavaScript() { last_click_ms = 0; last_click_pos = Point2(-100, -100); - window_maximized = false; - entering_fullscreen = false; - just_exited_fullscreen = false; transparency_enabled = false; main_loop = NULL; diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 5295e76a6163..208fd73dd35f 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -41,23 +41,32 @@ class OS_JavaScript : public OS_Unix { private: + struct JSTouchEvent { + uint32_t identifier[32] = { 0 }; + double coords[64] = { 0 }; + }; + JSTouchEvent touch_event; + + struct JSKeyEvent { + char code[32] = { 0 }; + char key[32] = { 0 }; + uint8_t modifiers[4] = { 0 }; + }; + JSKeyEvent key_event; + VideoMode video_mode; - bool window_maximized; - bool entering_fullscreen; - bool just_exited_fullscreen; bool transparency_enabled; EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_ctx; InputDefault *input; - Ref deferred_key_event; CursorShape cursor_shape; Point2 touches[32]; char canvas_id[256]; bool cursor_inside_canvas; Point2i last_click_pos; - double last_click_ms; + uint64_t last_click_ms; int last_click_button_index; MainLoop *main_loop; @@ -70,21 +79,12 @@ class OS_JavaScript : public OS_Unix { bool idb_needs_sync; bool idb_is_syncing; - static Point2 compute_position_in_canvas(int x, int y); - static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); - static EM_BOOL blur_callback(int p_event_type, const EmscriptenFocusEvent *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 void fullscreen_change_callback(int p_fullscreen); + static int mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers); + static void mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers); + static int mouse_wheel_callback(double p_delta_x, double p_delta_y); + static void key_callback(int p_pressed, int p_repeat, int p_modifiers); + static void touch_callback(int p_type, int p_count); static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); static void input_text_callback(const char *p_text, int p_cursor); @@ -93,6 +93,7 @@ class OS_JavaScript : public OS_Unix { static void file_access_close_callback(const String &p_file, int p_flags); static void request_quit_callback(); + static void window_blur_callback(); static void drop_files_callback(char **p_filev, int p_filec); static void send_notification_callback(int p_notification); static void fs_sync_callback();