Path: blob/master/platform/web/js/libs/library_godot_input.js
10279 views
/**************************************************************************/1/* library_godot_input.js */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930/*31* IME API helper.32*/3334const GodotIME = {35$GodotIME__deps: ['$GodotRuntime', '$GodotEventListeners'],36$GodotIME__postset: 'GodotOS.atexit(function(resolve, reject) { GodotIME.clear(); resolve(); });',37$GodotIME: {38ime: null,39active: false,40focusTimerIntervalId: -1,4142getModifiers: function (evt) {43return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);44},4546ime_active: function (active) {47function clearFocusTimerInterval() {48clearInterval(GodotIME.focusTimerIntervalId);49GodotIME.focusTimerIntervalId = -1;50}5152function focusTimer() {53if (GodotIME.ime == null) {54clearFocusTimerInterval();55return;56}57GodotIME.ime.focus();58}5960if (GodotIME.focusTimerIntervalId > -1) {61clearFocusTimerInterval();62}6364if (GodotIME.ime == null) {65return;66}6768GodotIME.active = active;69if (active) {70GodotIME.ime.style.display = 'block';71GodotIME.focusTimerIntervalId = setInterval(focusTimer, 100);72} else {73GodotIME.ime.style.display = 'none';74GodotConfig.canvas.focus();75}76},7778ime_position: function (x, y) {79if (GodotIME.ime == null) {80return;81}82const canvas = GodotConfig.canvas;83const rect = canvas.getBoundingClientRect();84const rw = canvas.width / rect.width;85const rh = canvas.height / rect.height;86const clx = (x / rw) + rect.x;87const cly = (y / rh) + rect.y;8889GodotIME.ime.style.left = `${clx}px`;90GodotIME.ime.style.top = `${cly}px`;91},9293init: function (ime_cb, key_cb, code, key) {94function key_event_cb(pressed, evt) {95const modifiers = GodotIME.getModifiers(evt);96GodotRuntime.stringToHeap(evt.code, code, 32);97GodotRuntime.stringToHeap(evt.key, key, 32);98key_cb(pressed, evt.repeat, modifiers);99evt.preventDefault();100}101function ime_event_cb(event) {102if (GodotIME.ime == null) {103return;104}105switch (event.type) {106case 'compositionstart':107ime_cb(0, null);108GodotIME.ime.innerHTML = '';109break;110case 'compositionupdate': {111const ptr = GodotRuntime.allocString(event.data);112ime_cb(1, ptr);113GodotRuntime.free(ptr);114} break;115case 'compositionend': {116const ptr = GodotRuntime.allocString(event.data);117ime_cb(2, ptr);118GodotRuntime.free(ptr);119GodotIME.ime.innerHTML = '';120} break;121default:122// Do nothing.123}124}125126const ime = document.createElement('div');127ime.className = 'ime';128ime.style.background = 'none';129ime.style.opacity = 0.0;130ime.style.position = 'fixed';131ime.style.textAlign = 'left';132ime.style.fontSize = '1px';133ime.style.left = '0px';134ime.style.top = '0px';135ime.style.width = '100%';136ime.style.height = '40px';137ime.style.pointerEvents = 'none';138ime.style.display = 'none';139ime.contentEditable = 'true';140141GodotEventListeners.add(ime, 'compositionstart', ime_event_cb, false);142GodotEventListeners.add(ime, 'compositionupdate', ime_event_cb, false);143GodotEventListeners.add(ime, 'compositionend', ime_event_cb, false);144GodotEventListeners.add(ime, 'keydown', key_event_cb.bind(null, 1), false);145GodotEventListeners.add(ime, 'keyup', key_event_cb.bind(null, 0), false);146147ime.onblur = function () {148this.style.display = 'none';149GodotConfig.canvas.focus();150GodotIME.active = false;151};152153GodotConfig.canvas.parentElement.appendChild(ime);154GodotIME.ime = ime;155},156157clear: function () {158if (GodotIME.ime == null) {159return;160}161if (GodotIME.focusTimerIntervalId > -1) {162clearInterval(GodotIME.focusTimerIntervalId);163GodotIME.focusTimerIntervalId = -1;164}165GodotIME.ime.remove();166GodotIME.ime = null;167},168},169};170mergeInto(LibraryManager.library, GodotIME);171172/*173* Gamepad API helper.174*/175const GodotInputGamepads = {176$GodotInputGamepads__deps: ['$GodotRuntime', '$GodotEventListeners'],177$GodotInputGamepads: {178samples: [],179180get_pads: function () {181try {182// Will throw in iframe when permission is denied.183// Will throw/warn in the future for insecure contexts.184// See https://github.com/w3c/gamepad/pull/120185const pads = navigator.getGamepads();186if (pads) {187return pads;188}189return [];190} catch (e) {191return [];192}193},194195get_samples: function () {196return GodotInputGamepads.samples;197},198199get_sample: function (index) {200const samples = GodotInputGamepads.samples;201return index < samples.length ? samples[index] : null;202},203204sample: function () {205const pads = GodotInputGamepads.get_pads();206const samples = [];207for (let i = 0; i < pads.length; i++) {208const pad = pads[i];209if (!pad) {210samples.push(null);211continue;212}213const s = {214standard: pad.mapping === 'standard',215buttons: [],216axes: [],217connected: pad.connected,218};219for (let b = 0; b < pad.buttons.length; b++) {220s.buttons.push(pad.buttons[b].value);221}222for (let a = 0; a < pad.axes.length; a++) {223s.axes.push(pad.axes[a]);224}225samples.push(s);226}227GodotInputGamepads.samples = samples;228},229230init: function (onchange) {231GodotInputGamepads.samples = [];232function add(pad) {233const guid = GodotInputGamepads.get_guid(pad);234const c_id = GodotRuntime.allocString(pad.id);235const c_guid = GodotRuntime.allocString(guid);236onchange(pad.index, 1, c_id, c_guid);237GodotRuntime.free(c_id);238GodotRuntime.free(c_guid);239}240const pads = GodotInputGamepads.get_pads();241for (let i = 0; i < pads.length; i++) {242// Might be reserved space.243if (pads[i]) {244add(pads[i]);245}246}247GodotEventListeners.add(window, 'gamepadconnected', function (evt) {248if (evt.gamepad) {249add(evt.gamepad);250}251}, false);252GodotEventListeners.add(window, 'gamepaddisconnected', function (evt) {253if (evt.gamepad) {254onchange(evt.gamepad.index, 0);255}256}, false);257},258259get_guid: function (pad) {260if (pad.mapping) {261return pad.mapping;262}263const ua = navigator.userAgent;264let os = 'Unknown';265if (ua.indexOf('Android') >= 0) {266os = 'Android';267} else if (ua.indexOf('Linux') >= 0) {268os = 'Linux';269} else if (ua.indexOf('iPhone') >= 0) {270os = 'iOS';271} else if (ua.indexOf('Macintosh') >= 0) {272// Updated iPads will fall into this category.273os = 'MacOSX';274} else if (ua.indexOf('Windows') >= 0) {275os = 'Windows';276}277278const id = pad.id;279// Chrom* style: NAME (Vendor: xxxx Product: xxxx).280const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i;281// Firefox/Safari style (Safari may remove leading zeroes).282const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i;283let vendor = '';284let product = '';285if (exp1.test(id)) {286const match = exp1.exec(id);287vendor = match[1].padStart(4, '0');288product = match[2].padStart(4, '0');289} else if (exp2.test(id)) {290const match = exp2.exec(id);291vendor = match[1].padStart(4, '0');292product = match[2].padStart(4, '0');293}294if (!vendor || !product) {295return `${os}Unknown`;296}297return os + vendor + product;298},299},300};301mergeInto(LibraryManager.library, GodotInputGamepads);302303/*304* Drag and drop helper.305* This is pretty big, but basically detect dropped files on GodotConfig.canvas,306* process them one by one (recursively for directories), and copies them to307* the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot308* event (that requires a string array of paths).309*310* NOTE: The temporary files are removed after the callback. This means that311* deferred callbacks won't be able to access the files.312*/313const GodotInputDragDrop = {314$GodotInputDragDrop__deps: ['$FS', '$GodotFS'],315$GodotInputDragDrop: {316promises: [],317pending_files: [],318319add_entry: function (entry) {320if (entry.isDirectory) {321GodotInputDragDrop.add_dir(entry);322} else if (entry.isFile) {323GodotInputDragDrop.add_file(entry);324} else {325GodotRuntime.error('Unrecognized entry...', entry);326}327},328329add_dir: function (entry) {330GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) {331const reader = entry.createReader();332reader.readEntries(function (entries) {333for (let i = 0; i < entries.length; i++) {334GodotInputDragDrop.add_entry(entries[i]);335}336resolve();337});338}));339},340341add_file: function (entry) {342GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) {343entry.file(function (file) {344const reader = new FileReader();345reader.onload = function () {346const f = {347'path': file.relativePath || file.webkitRelativePath,348'name': file.name,349'type': file.type,350'size': file.size,351'data': reader.result,352};353if (!f['path']) {354f['path'] = f['name'];355}356GodotInputDragDrop.pending_files.push(f);357resolve();358};359reader.onerror = function () {360GodotRuntime.print('Error reading file');361reject();362};363reader.readAsArrayBuffer(file);364}, function (err) {365GodotRuntime.print('Error!');366reject();367});368}));369},370371process: function (resolve, reject) {372if (GodotInputDragDrop.promises.length === 0) {373resolve();374return;375}376GodotInputDragDrop.promises.pop().then(function () {377setTimeout(function () {378GodotInputDragDrop.process(resolve, reject);379}, 0);380});381},382383_process_event: function (ev, callback) {384ev.preventDefault();385if (ev.dataTransfer.items) {386// Use DataTransferItemList interface to access the file(s)387for (let i = 0; i < ev.dataTransfer.items.length; i++) {388const item = ev.dataTransfer.items[i];389let entry = null;390if ('getAsEntry' in item) {391entry = item.getAsEntry();392} else if ('webkitGetAsEntry' in item) {393entry = item.webkitGetAsEntry();394}395if (entry) {396GodotInputDragDrop.add_entry(entry);397}398}399} else {400GodotRuntime.error('File upload not supported');401}402new Promise(GodotInputDragDrop.process).then(function () {403const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`;404const drops = [];405const files = [];406FS.mkdir(DROP.slice(0, -1)); // Without trailing slash407GodotInputDragDrop.pending_files.forEach((elem) => {408const path = elem['path'];409GodotFS.copy_to_fs(DROP + path, elem['data']);410let idx = path.indexOf('/');411if (idx === -1) {412// Root file413drops.push(DROP + path);414} else {415// Subdir416const sub = path.substr(0, idx);417idx = sub.indexOf('/');418if (idx < 0 && drops.indexOf(DROP + sub) === -1) {419drops.push(DROP + sub);420}421}422files.push(DROP + path);423});424GodotInputDragDrop.promises = [];425GodotInputDragDrop.pending_files = [];426callback(drops);427if (GodotConfig.persistent_drops) {428// Delay removal at exit.429GodotOS.atexit(function (resolve, reject) {430GodotInputDragDrop.remove_drop(files, DROP);431resolve();432});433} else {434GodotInputDragDrop.remove_drop(files, DROP);435}436});437},438439remove_drop: function (files, drop_path) {440const dirs = [drop_path.substr(0, drop_path.length - 1)];441// Remove temporary files442files.forEach(function (file) {443FS.unlink(file);444let dir = file.replace(drop_path, '');445let idx = dir.lastIndexOf('/');446while (idx > 0) {447dir = dir.substr(0, idx);448if (dirs.indexOf(drop_path + dir) === -1) {449dirs.push(drop_path + dir);450}451idx = dir.lastIndexOf('/');452}453});454// Remove dirs.455dirs.sort(function (a, b) {456const al = (a.match(/\//g) || []).length;457const bl = (b.match(/\//g) || []).length;458if (al > bl) {459return -1;460} else if (al < bl) {461return 1;462}463return 0;464}).forEach(function (dir) {465FS.rmdir(dir);466});467},468469handler: function (callback) {470return function (ev) {471GodotInputDragDrop._process_event(ev, callback);472};473},474},475};476mergeInto(LibraryManager.library, GodotInputDragDrop);477478/*479* Godot exposed input functions.480*/481const GodotInput = {482$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop', '$GodotIME'],483$GodotInput: {484getModifiers: function (evt) {485return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);486},487computePosition: function (evt, rect) {488const canvas = GodotConfig.canvas;489const rw = canvas.width / rect.width;490const rh = canvas.height / rect.height;491const x = (evt.clientX - rect.x) * rw;492const y = (evt.clientY - rect.y) * rh;493return [x, y];494},495},496497/*498* Mouse API499*/500godot_js_input_mouse_move_cb__proxy: 'sync',501godot_js_input_mouse_move_cb__sig: 'vi',502godot_js_input_mouse_move_cb: function (callback) {503const func = GodotRuntime.get_func(callback);504const canvas = GodotConfig.canvas;505function move_cb(evt) {506const rect = canvas.getBoundingClientRect();507const pos = GodotInput.computePosition(evt, rect);508// Scale movement509const rw = canvas.width / rect.width;510const rh = canvas.height / rect.height;511const rel_pos_x = evt.movementX * rw;512const rel_pos_y = evt.movementY * rh;513const modifiers = GodotInput.getModifiers(evt);514func(pos[0], pos[1], rel_pos_x, rel_pos_y, modifiers, evt.pressure);515}516GodotEventListeners.add(window, 'pointermove', move_cb, false);517},518519godot_js_input_mouse_wheel_cb__proxy: 'sync',520godot_js_input_mouse_wheel_cb__sig: 'vi',521godot_js_input_mouse_wheel_cb: function (callback) {522const func = GodotRuntime.get_func(callback);523function wheel_cb(evt) {524if (func(evt.deltaMode, evt.deltaX ?? 0, evt.deltaY ?? 0)) {525evt.preventDefault();526}527}528GodotEventListeners.add(GodotConfig.canvas, 'wheel', wheel_cb, false);529},530531godot_js_input_mouse_button_cb__proxy: 'sync',532godot_js_input_mouse_button_cb__sig: 'vi',533godot_js_input_mouse_button_cb: function (callback) {534const func = GodotRuntime.get_func(callback);535const canvas = GodotConfig.canvas;536function button_cb(p_pressed, evt) {537const rect = canvas.getBoundingClientRect();538const pos = GodotInput.computePosition(evt, rect);539const modifiers = GodotInput.getModifiers(evt);540// Since the event is consumed, focus manually.541// NOTE: The iframe container may not have focus yet, so focus even when already active.542if (p_pressed) {543GodotConfig.canvas.focus();544}545if (func(p_pressed, evt.button, pos[0], pos[1], modifiers)) {546evt.preventDefault();547}548}549GodotEventListeners.add(canvas, 'mousedown', button_cb.bind(null, 1), false);550GodotEventListeners.add(window, 'mouseup', button_cb.bind(null, 0), false);551},552553/*554* Touch API555*/556godot_js_input_touch_cb__proxy: 'sync',557godot_js_input_touch_cb__sig: 'viii',558godot_js_input_touch_cb: function (callback, ids, coords) {559const func = GodotRuntime.get_func(callback);560const canvas = GodotConfig.canvas;561function touch_cb(type, evt) {562// Since the event is consumed, focus manually.563// NOTE: The iframe container may not have focus yet, so focus even when already active.564if (type === 0) {565GodotConfig.canvas.focus();566}567const rect = canvas.getBoundingClientRect();568const touches = evt.changedTouches;569for (let i = 0; i < touches.length; i++) {570const touch = touches[i];571const pos = GodotInput.computePosition(touch, rect);572GodotRuntime.setHeapValue(coords + (i * 2) * 8, pos[0], 'double');573GodotRuntime.setHeapValue(coords + (i * 2 + 1) * 8, pos[1], 'double');574GodotRuntime.setHeapValue(ids + i * 4, touch.identifier, 'i32');575}576func(type, touches.length);577if (evt.cancelable) {578evt.preventDefault();579}580}581GodotEventListeners.add(canvas, 'touchstart', touch_cb.bind(null, 0), false);582GodotEventListeners.add(canvas, 'touchend', touch_cb.bind(null, 1), false);583GodotEventListeners.add(canvas, 'touchcancel', touch_cb.bind(null, 1), false);584GodotEventListeners.add(canvas, 'touchmove', touch_cb.bind(null, 2), false);585},586587/*588* Key API589*/590godot_js_input_key_cb__proxy: 'sync',591godot_js_input_key_cb__sig: 'viii',592godot_js_input_key_cb: function (callback, code, key) {593const func = GodotRuntime.get_func(callback);594function key_cb(pressed, evt) {595const modifiers = GodotInput.getModifiers(evt);596GodotRuntime.stringToHeap(evt.code, code, 32);597GodotRuntime.stringToHeap(evt.key, key, 32);598func(pressed, evt.repeat, modifiers);599evt.preventDefault();600}601GodotEventListeners.add(GodotConfig.canvas, 'keydown', key_cb.bind(null, 1), false);602GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false);603},604605/*606* IME API607*/608godot_js_set_ime_active__proxy: 'sync',609godot_js_set_ime_active__sig: 'vi',610godot_js_set_ime_active: function (p_active) {611GodotIME.ime_active(p_active);612},613614godot_js_set_ime_position__proxy: 'sync',615godot_js_set_ime_position__sig: 'vii',616godot_js_set_ime_position: function (p_x, p_y) {617GodotIME.ime_position(p_x, p_y);618},619620godot_js_set_ime_cb__proxy: 'sync',621godot_js_set_ime_cb__sig: 'viiii',622godot_js_set_ime_cb: function (p_ime_cb, p_key_cb, code, key) {623const ime_cb = GodotRuntime.get_func(p_ime_cb);624const key_cb = GodotRuntime.get_func(p_key_cb);625GodotIME.init(ime_cb, key_cb, code, key);626},627628godot_js_is_ime_focused__proxy: 'sync',629godot_js_is_ime_focused__sig: 'i',630godot_js_is_ime_focused: function () {631return GodotIME.active;632},633634/*635* Gamepad API636*/637godot_js_input_gamepad_cb__proxy: 'sync',638godot_js_input_gamepad_cb__sig: 'vi',639godot_js_input_gamepad_cb: function (change_cb) {640const onchange = GodotRuntime.get_func(change_cb);641GodotInputGamepads.init(onchange);642},643644godot_js_input_gamepad_sample_count__proxy: 'sync',645godot_js_input_gamepad_sample_count__sig: 'i',646godot_js_input_gamepad_sample_count: function () {647return GodotInputGamepads.get_samples().length;648},649650godot_js_input_gamepad_sample__proxy: 'sync',651godot_js_input_gamepad_sample__sig: 'i',652godot_js_input_gamepad_sample: function () {653GodotInputGamepads.sample();654return 0;655},656657godot_js_input_gamepad_sample_get__proxy: 'sync',658godot_js_input_gamepad_sample_get__sig: 'iiiiiii',659godot_js_input_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) {660const sample = GodotInputGamepads.get_sample(p_index);661if (!sample || !sample.connected) {662return 1;663}664const btns = sample.buttons;665const btns_len = btns.length < 16 ? btns.length : 16;666for (let i = 0; i < btns_len; i++) {667GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float');668}669GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32');670const axes = sample.axes;671const axes_len = axes.length < 10 ? axes.length : 10;672for (let i = 0; i < axes_len; i++) {673GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float');674}675GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32');676const is_standard = sample.standard ? 1 : 0;677GodotRuntime.setHeapValue(r_standard, is_standard, 'i32');678return 0;679},680681/*682* Drag/Drop API683*/684godot_js_input_drop_files_cb__proxy: 'sync',685godot_js_input_drop_files_cb__sig: 'vi',686godot_js_input_drop_files_cb: function (callback) {687const func = GodotRuntime.get_func(callback);688const dropFiles = function (files) {689const args = files || [];690if (!args.length) {691return;692}693const argc = args.length;694const argv = GodotRuntime.allocStringArray(args);695func(argv, argc);696GodotRuntime.freeStringArray(argv, argc);697};698const canvas = GodotConfig.canvas;699GodotEventListeners.add(canvas, 'dragover', function (ev) {700// Prevent default behavior (which would try to open the file(s))701ev.preventDefault();702}, false);703GodotEventListeners.add(canvas, 'drop', GodotInputDragDrop.handler(dropFiles));704},705706/* Paste API */707godot_js_input_paste_cb__proxy: 'sync',708godot_js_input_paste_cb__sig: 'vi',709godot_js_input_paste_cb: function (callback) {710const func = GodotRuntime.get_func(callback);711GodotEventListeners.add(window, 'paste', function (evt) {712const text = evt.clipboardData.getData('text');713const ptr = GodotRuntime.allocString(text);714func(ptr);715GodotRuntime.free(ptr);716}, false);717},718719godot_js_input_vibrate_handheld__proxy: 'sync',720godot_js_input_vibrate_handheld__sig: 'vi',721godot_js_input_vibrate_handheld: function (p_duration_ms) {722if (typeof navigator.vibrate !== 'function') {723GodotRuntime.print('This browser does not support vibration.');724} else {725navigator.vibrate(p_duration_ms);726}727},728};729730autoAddDeps(GodotInput, '$GodotInput');731mergeInto(LibraryManager.library, GodotInput);732733734