Browse Source

[Web] Add IME input support.

bruvzg 2 years ago
parent
commit
f9486a2d88

+ 140 - 13
platform/web/display_server_web.cpp

@@ -174,6 +174,11 @@ void DisplayServerWeb::_key_callback(const String &p_key_event_code, const Strin
 	// Resume audio context after input in case autoplay was denied.
 	// Resume audio context after input in case autoplay was denied.
 	OS_Web::get_singleton()->resume_audio();
 	OS_Web::get_singleton()->resume_audio();
 
 
+	DisplayServerWeb *ds = get_singleton();
+	if (ds->ime_started) {
+		return;
+	}
+
 	char32_t c = 0x00;
 	char32_t c = 0x00;
 	String unicode = p_key_event_key;
 	String unicode = p_key_event_key;
 	if (unicode.length() == 1) {
 	if (unicode.length() == 1) {
@@ -183,17 +188,21 @@ void DisplayServerWeb::_key_callback(const String &p_key_event_code, const Strin
 	Key keycode = dom_code2godot_scancode(p_key_event_code.utf8().get_data(), p_key_event_key.utf8().get_data(), false);
 	Key keycode = dom_code2godot_scancode(p_key_event_code.utf8().get_data(), p_key_event_key.utf8().get_data(), false);
 	Key scancode = dom_code2godot_scancode(p_key_event_code.utf8().get_data(), p_key_event_key.utf8().get_data(), true);
 	Key scancode = dom_code2godot_scancode(p_key_event_code.utf8().get_data(), p_key_event_key.utf8().get_data(), true);
 
 
-	Ref<InputEventKey> ev;
-	ev.instantiate();
-	ev->set_echo(p_repeat);
-	ev->set_keycode(fix_keycode(c, keycode));
-	ev->set_physical_keycode(scancode);
-	ev->set_key_label(fix_key_label(c, keycode));
-	ev->set_unicode(fix_unicode(c));
-	ev->set_pressed(p_pressed);
-	dom2godot_mod(ev, p_modifiers, fix_keycode(c, keycode));
+	DisplayServerWeb::KeyEvent ke;
 
 
-	Input::get_singleton()->parse_input_event(ev);
+	ke.pressed = p_pressed;
+	ke.echo = p_repeat;
+	ke.raw = true;
+	ke.keycode = fix_keycode(c, keycode);
+	ke.physical_keycode = scancode;
+	ke.key_label = fix_key_label(c, keycode);
+	ke.unicode = fix_unicode(c);
+	ke.mod = p_modifiers;
+
+	if (ds->key_event_pos >= ds->key_event_buffer.size()) {
+		ds->key_event_buffer.resize(1 + ds->key_event_pos);
+	}
+	ds->key_event_buffer.write[ds->key_event_pos++] = ke;
 
 
 	// Make sure to flush all events so we can call restricted APIs inside the event.
 	// Make sure to flush all events so we can call restricted APIs inside the event.
 	Input::get_singleton()->flush_buffered_events();
 	Input::get_singleton()->flush_buffered_events();
@@ -619,7 +628,7 @@ int DisplayServerWeb::mouse_wheel_callback(double p_delta_x, double p_delta_y) {
 }
 }
 
 
 int DisplayServerWeb::_mouse_wheel_callback(double p_delta_x, double p_delta_y) {
 int DisplayServerWeb::_mouse_wheel_callback(double p_delta_x, double p_delta_y) {
-	if (!godot_js_display_canvas_is_focused()) {
+	if (!godot_js_display_canvas_is_focused() && !godot_js_is_ime_focused()) {
 		if (get_singleton()->cursor_inside_canvas) {
 		if (get_singleton()->cursor_inside_canvas) {
 			godot_js_display_canvas_focus();
 			godot_js_display_canvas_focus();
 		} else {
 		} else {
@@ -726,7 +735,7 @@ bool DisplayServerWeb::is_touchscreen_available() const {
 
 
 // Virtual Keyboard
 // Virtual Keyboard
 void DisplayServerWeb::vk_input_text_callback(const char *p_text, int p_cursor) {
 void DisplayServerWeb::vk_input_text_callback(const char *p_text, int p_cursor) {
-	String text = p_text;
+	String text = String::utf8(p_text);
 
 
 #ifdef PROXY_TO_PTHREAD_ENABLED
 #ifdef PROXY_TO_PTHREAD_ENABLED
 	if (!Thread::is_main_thread()) {
 	if (!Thread::is_main_thread()) {
@@ -809,6 +818,100 @@ void DisplayServerWeb::_gamepad_callback(int p_index, int p_connected, const Str
 	}
 	}
 }
 }
 
 
+// IME.
+void DisplayServerWeb::ime_callback(int p_type, const char *p_text) {
+	String text = String::utf8(p_text);
+
+#ifdef PROXY_TO_PTHREAD_ENABLED
+	if (!Thread::is_main_thread()) {
+		callable_mp_static(DisplayServerWeb::_ime_callback).bind(p_type, text).call_deferred();
+		return;
+	}
+#endif
+
+	_ime_callback(p_type, text);
+}
+
+void DisplayServerWeb::_ime_callback(int p_type, const String &p_text) {
+	DisplayServerWeb *ds = get_singleton();
+	// Resume audio context after input in case autoplay was denied.
+	OS_Web::get_singleton()->resume_audio();
+
+	switch (p_type) {
+		case 0: {
+			// IME start.
+			ds->ime_text = String();
+			ds->ime_selection = Vector2i();
+			for (int i = ds->key_event_pos - 1; i >= 0; i--) {
+				// Delete last raw keydown event from query.
+				if (ds->key_event_buffer[i].pressed && ds->key_event_buffer[i].raw) {
+					ds->key_event_buffer.remove_at(i);
+					ds->key_event_pos--;
+					break;
+				}
+			}
+			OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE);
+			ds->ime_started = true;
+		} break;
+		case 1: {
+			// IME update.
+			if (ds->ime_active && ds->ime_started) {
+				ds->ime_text = p_text;
+				ds->ime_selection = Vector2i(ds->ime_text.length(), ds->ime_text.length());
+				OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE);
+			}
+		} break;
+		case 2: {
+			// IME commit.
+			if (ds->ime_active && ds->ime_started) {
+				ds->ime_started = false;
+
+				ds->ime_text = String();
+				ds->ime_selection = Vector2i();
+				OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE);
+
+				String text = p_text;
+				for (int i = 0; i < text.length(); i++) {
+					DisplayServerWeb::KeyEvent ke;
+
+					ke.pressed = true;
+					ke.echo = false;
+					ke.raw = false;
+					ke.keycode = Key::NONE;
+					ke.physical_keycode = Key::NONE;
+					ke.key_label = Key::NONE;
+					ke.unicode = text[i];
+					ke.mod = 0;
+
+					if (ds->key_event_pos >= ds->key_event_buffer.size()) {
+						ds->key_event_buffer.resize(1 + ds->key_event_pos);
+					}
+					ds->key_event_buffer.write[ds->key_event_pos++] = ke;
+				}
+			}
+		} break;
+		default:
+			break;
+	}
+}
+
+void DisplayServerWeb::window_set_ime_active(const bool p_active, WindowID p_window) {
+	ime_active = p_active;
+	godot_js_set_ime_active(p_active);
+}
+
+void DisplayServerWeb::window_set_ime_position(const Point2i &p_pos, WindowID p_window) {
+	godot_js_set_ime_position(p_pos.x, p_pos.y);
+}
+
+Point2i DisplayServerWeb::ime_get_selection() const {
+	return ime_selection;
+}
+
+String DisplayServerWeb::ime_get_text() const {
+	return ime_text;
+}
+
 void DisplayServerWeb::process_joypads() {
 void DisplayServerWeb::process_joypads() {
 	Input *input = Input::get_singleton();
 	Input *input = Input::get_singleton();
 	int32_t pads = godot_js_input_gamepad_sample_count();
 	int32_t pads = godot_js_input_gamepad_sample_count();
@@ -893,6 +996,9 @@ void DisplayServerWeb::_send_window_event_callback(int p_notification) {
 	if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) {
 	if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) {
 		ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER;
 		ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER;
 	}
 	}
+	if (godot_js_is_ime_focused() && (p_notification == DisplayServer::WINDOW_EVENT_FOCUS_IN || p_notification == DisplayServer::WINDOW_EVENT_FOCUS_OUT)) {
+		return;
+	}
 	if (!ds->window_event_callback.is_null()) {
 	if (!ds->window_event_callback.is_null()) {
 		Variant event = int(p_notification);
 		Variant event = int(p_notification);
 		ds->window_event_callback.call(event);
 		ds->window_event_callback.call(event);
@@ -1003,6 +1109,7 @@ DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode
 	godot_js_input_paste_cb(&DisplayServerWeb::update_clipboard_callback);
 	godot_js_input_paste_cb(&DisplayServerWeb::update_clipboard_callback);
 	godot_js_input_drop_files_cb(&DisplayServerWeb::drop_files_js_callback);
 	godot_js_input_drop_files_cb(&DisplayServerWeb::drop_files_js_callback);
 	godot_js_input_gamepad_cb(&DisplayServerWeb::gamepad_callback);
 	godot_js_input_gamepad_cb(&DisplayServerWeb::gamepad_callback);
+	godot_js_set_ime_cb(&DisplayServerWeb::ime_callback, &DisplayServerWeb::key_callback, key_event.code, key_event.key);
 
 
 	// JS Display interface (js/libs/library_godot_display.js)
 	// JS Display interface (js/libs/library_godot_display.js)
 	godot_js_display_fullscreen_cb(&DisplayServerWeb::fullscreen_change_callback);
 	godot_js_display_fullscreen_cb(&DisplayServerWeb::fullscreen_change_callback);
@@ -1030,7 +1137,6 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
 	switch (p_feature) {
 	switch (p_feature) {
 		//case FEATURE_GLOBAL_MENU:
 		//case FEATURE_GLOBAL_MENU:
 		//case FEATURE_HIDPI:
 		//case FEATURE_HIDPI:
-		//case FEATURE_IME:
 		case FEATURE_ICON:
 		case FEATURE_ICON:
 		case FEATURE_CLIPBOARD:
 		case FEATURE_CLIPBOARD:
 		case FEATURE_CURSOR_SHAPE:
 		case FEATURE_CURSOR_SHAPE:
@@ -1044,6 +1150,9 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_KEEP_SCREEN_ON:
 		//case FEATURE_KEEP_SCREEN_ON:
 		//case FEATURE_ORIENTATION:
 		//case FEATURE_ORIENTATION:
+		case FEATURE_IME:
+			// IME does not work with experimental VK support.
+			return godot_js_display_vk_available() == 0;
 		case FEATURE_VIRTUAL_KEYBOARD:
 		case FEATURE_VIRTUAL_KEYBOARD:
 			return godot_js_display_vk_available() != 0;
 			return godot_js_display_vk_available() != 0;
 		case FEATURE_TEXT_TO_SPEECH:
 		case FEATURE_TEXT_TO_SPEECH:
@@ -1263,6 +1372,24 @@ void DisplayServerWeb::process_events() {
 	Input::get_singleton()->flush_buffered_events();
 	Input::get_singleton()->flush_buffered_events();
 	if (godot_js_input_gamepad_sample() == OK) {
 	if (godot_js_input_gamepad_sample() == OK) {
 		process_joypads();
 		process_joypads();
+		for (int i = 0; i < key_event_pos; i++) {
+			const DisplayServerWeb::KeyEvent &ke = key_event_buffer[i];
+
+			Ref<InputEventKey> ev;
+			ev.instantiate();
+			ev->set_pressed(ke.pressed);
+			ev->set_echo(ke.echo);
+			ev->set_keycode(ke.keycode);
+			ev->set_physical_keycode(ke.physical_keycode);
+			ev->set_key_label(ke.key_label);
+			ev->set_unicode(ke.unicode);
+			if (ke.raw) {
+				dom2godot_mod(ev, ke.mod, ke.keycode);
+			}
+
+			Input::get_singleton()->parse_input_event(ev);
+		}
+		key_event_pos = 0;
 	}
 	}
 }
 }
 
 

+ 28 - 0
platform/web/display_server_web.h

@@ -82,6 +82,25 @@ private:
 	uint64_t last_click_ms = 0;
 	uint64_t last_click_ms = 0;
 	MouseButton last_click_button_index = MouseButton::NONE;
 	MouseButton last_click_button_index = MouseButton::NONE;
 
 
+	bool ime_active = false;
+	bool ime_started = false;
+	String ime_text;
+	Vector2i ime_selection;
+
+	struct KeyEvent {
+		bool pressed = false;
+		bool echo = false;
+		bool raw = false;
+		Key keycode = Key::NONE;
+		Key physical_keycode = Key::NONE;
+		Key key_label = Key::NONE;
+		uint32_t unicode = 0;
+		int mod = 0;
+	};
+
+	Vector<KeyEvent> key_event_buffer;
+	int key_event_pos = 0;
+
 	bool swap_cancel_ok = false;
 	bool swap_cancel_ok = false;
 	bool tts = false;
 	bool tts = false;
 
 
@@ -108,6 +127,8 @@ private:
 	static void _gamepad_callback(int p_index, int p_connected, const String &p_id, const String &p_guid);
 	static void _gamepad_callback(int p_index, int p_connected, const String &p_id, const String &p_guid);
 	WASM_EXPORT static void js_utterance_callback(int p_event, int p_id, int p_pos);
 	WASM_EXPORT static void js_utterance_callback(int p_event, int p_id, int p_pos);
 	static void _js_utterance_callback(int p_event, int p_id, int p_pos);
 	static void _js_utterance_callback(int p_event, int p_id, int p_pos);
+	WASM_EXPORT static void ime_callback(int p_type, const char *p_text);
+	static void _ime_callback(int p_type, const String &p_text);
 	WASM_EXPORT static void request_quit_callback();
 	WASM_EXPORT static void request_quit_callback();
 	static void _request_quit_callback();
 	static void _request_quit_callback();
 	WASM_EXPORT static void window_blur_callback();
 	WASM_EXPORT static void window_blur_callback();
@@ -162,6 +183,13 @@ public:
 	virtual MouseMode mouse_get_mode() const override;
 	virtual MouseMode mouse_get_mode() const override;
 	virtual Point2i mouse_get_position() const override;
 	virtual Point2i mouse_get_position() const override;
 
 
+	// ime
+	virtual void window_set_ime_active(const bool p_active, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_ime_position(const Point2i &p_pos, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	virtual Point2i ime_get_selection() const override;
+	virtual String ime_get_text() const override;
+
 	// touch
 	// touch
 	virtual bool is_touchscreen_available() const override;
 	virtual bool is_touchscreen_available() const override;
 
 

+ 5 - 0
platform/web/godot_js.h

@@ -64,6 +64,11 @@ extern void godot_js_input_touch_cb(void (*p_callback)(int p_type, int p_count),
 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]);
 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]);
 extern void godot_js_input_vibrate_handheld(int p_duration_ms);
 extern void godot_js_input_vibrate_handheld(int p_duration_ms);
 
 
+extern void godot_js_set_ime_active(int p_active);
+extern void godot_js_set_ime_position(int p_x, int p_y);
+extern void godot_js_set_ime_cb(void (*p_input)(int p_type, const char *p_text), void (*p_callback)(int p_type, int p_repeat, int p_modifiers), char r_code[32], char r_key[32]);
+extern int godot_js_is_ime_focused();
+
 // Input gamepad
 // 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 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();

+ 134 - 1
platform/web/js/libs/library_godot_input.js

@@ -28,6 +28,110 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /**************************************************************************/
 /**************************************************************************/
 
 
+/*
+ * IME API helper.
+ */
+
+const GodotIME = {
+	$GodotIME__deps: ['$GodotRuntime', '$GodotEventListeners'],
+	$GodotIME__postset: 'GodotOS.atexit(function(resolve, reject) { GodotIME.clear(); resolve(); });',
+	$GodotIME: {
+		ime: null,
+		active: false,
+
+		getModifiers: function (evt) {
+			return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);
+		},
+
+		ime_active: function (active) {
+			function focus_timer() {
+				GodotIME.active = true;
+				GodotIME.ime.focus();
+			}
+
+			if (GodotIME.ime) {
+				if (active) {
+					GodotIME.ime.style.display = 'block';
+					setInterval(focus_timer, 100);
+				} else {
+					GodotIME.ime.style.display = 'none';
+					GodotConfig.canvas.focus();
+					GodotIME.active = false;
+				}
+			}
+		},
+
+		ime_position: function (x, y) {
+			if (GodotIME.ime) {
+				GodotIME.ime.style.left = `${x}px`;
+				GodotIME.ime.style.top = `${y}px`;
+			}
+		},
+
+		init: function (ime_cb, key_cb, code, key) {
+			function key_event_cb(pressed, evt) {
+				const modifiers = GodotIME.getModifiers(evt);
+				GodotRuntime.stringToHeap(evt.code, code, 32);
+				GodotRuntime.stringToHeap(evt.key, key, 32);
+				key_cb(pressed, evt.repeat, modifiers);
+				evt.preventDefault();
+			}
+			function ime_event_cb(event) {
+				if (GodotIME.ime) {
+					if (event.type === 'compositionstart') {
+						ime_cb(0, null);
+						GodotIME.ime.innerHTML = '';
+					} else if (event.type === 'compositionupdate') {
+						const ptr = GodotRuntime.allocString(event.data);
+						ime_cb(1, ptr);
+						GodotRuntime.free(ptr);
+					} else if (event.type === 'compositionend') {
+						const ptr = GodotRuntime.allocString(event.data);
+						ime_cb(2, ptr);
+						GodotRuntime.free(ptr);
+						GodotIME.ime.innerHTML = '';
+					}
+				}
+			}
+
+			const ime = document.createElement('div');
+			ime.className = 'ime';
+			ime.style.background = 'none';
+			ime.style.opacity = 0.0;
+			ime.style.position = 'fixed';
+			ime.style.left = '0px';
+			ime.style.top = '0px';
+			ime.style.width = '2px';
+			ime.style.height = '2px';
+			ime.style.display = 'none';
+			ime.contentEditable = 'true';
+
+			GodotEventListeners.add(ime, 'compositionstart', ime_event_cb, false);
+			GodotEventListeners.add(ime, 'compositionupdate', ime_event_cb, false);
+			GodotEventListeners.add(ime, 'compositionend', ime_event_cb, false);
+			GodotEventListeners.add(ime, 'keydown', key_event_cb.bind(null, 1), false);
+			GodotEventListeners.add(ime, 'keyup', key_event_cb.bind(null, 0), false);
+
+			ime.onblur = function () {
+				this.style.display = 'none';
+				GodotConfig.canvas.focus();
+				GodotIME.active = false;
+			};
+
+			GodotConfig.canvas.parentElement.appendChild(ime);
+			GodotIME.ime = ime;
+		},
+
+		clear: function () {
+			if (GodotIME.ime) {
+				GodotIME.ime.remove();
+				GodotIME.ime = null;
+			}
+		},
+	},
+};
+mergeInto(LibraryManager.library, GodotIME);
+
 /*
 /*
  * Gamepad API helper.
  * Gamepad API helper.
  */
  */
@@ -338,7 +442,7 @@ mergeInto(LibraryManager.library, GodotInputDragDrop);
  * Godot exposed input functions.
  * Godot exposed input functions.
  */
  */
 const GodotInput = {
 const GodotInput = {
-	$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop'],
+	$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop', '$GodotIME'],
 	$GodotInput: {
 	$GodotInput: {
 		getModifiers: function (evt) {
 		getModifiers: function (evt) {
 			return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);
 			return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);
@@ -461,6 +565,35 @@ const GodotInput = {
 		GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false);
 		GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false);
 	},
 	},
 
 
+	/*
+	 * IME API
+	 */
+	godot_js_set_ime_active__proxy: 'sync',
+	godot_js_set_ime_active__sig: 'vi',
+	godot_js_set_ime_active: function (p_active) {
+		GodotIME.ime_active(p_active);
+	},
+
+	godot_js_set_ime_position__proxy: 'sync',
+	godot_js_set_ime_position__sig: 'vii',
+	godot_js_set_ime_position: function (p_x, p_y) {
+		GodotIME.ime_position(p_x, p_y);
+	},
+
+	godot_js_set_ime_cb__proxy: 'sync',
+	godot_js_set_ime_cb__sig: 'viiii',
+	godot_js_set_ime_cb: function (p_ime_cb, p_key_cb, code, key) {
+		const ime_cb = GodotRuntime.get_func(p_ime_cb);
+		const key_cb = GodotRuntime.get_func(p_key_cb);
+		GodotIME.init(ime_cb, key_cb, code, key);
+	},
+
+	godot_js_is_ime_focused__proxy: 'sync',
+	godot_js_is_ime_focused__sig: 'i',
+	godot_js_is_ime_focused: function () {
+		return GodotIME.active;
+	},
+
 	/*
 	/*
 	 * Gamepad API
 	 * Gamepad API
 	 */
 	 */