Skip to content

Commit

Permalink
[Web] Add IME input support.
Browse files Browse the repository at this point in the history
  • Loading branch information
bruvzg committed Dec 1, 2023
1 parent d76c1d0 commit 941ffe0
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 4 deletions.
112 changes: 109 additions & 3 deletions platform/web/display_server_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,15 @@ void DisplayServerWeb::_key_callback(const String &p_key_event_code, const Strin
ev->set_pressed(p_pressed);
dom2godot_mod(ev, p_modifiers, fix_keycode(c, keycode));

Input::get_singleton()->parse_input_event(ev);
DisplayServerWeb *ds = get_singleton();
if (ds->ime_active) {
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++] = ev;
} else {
Input::get_singleton()->parse_input_event(ev);
}

// Make sure to flush all events so we can call restricted APIs inside the event.
Input::get_singleton()->flush_buffered_events();
Expand Down Expand Up @@ -726,7 +734,7 @@ bool DisplayServerWeb::is_touchscreen_available() const {

// Virtual Keyboard
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
if (!Thread::is_main_thread()) {
Expand Down Expand Up @@ -809,6 +817,98 @@ 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 keydown event from query.
if (ds->key_event_buffer[i]->is_pressed()) {
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_active = true;
} break;
case 1: {
// IME update.
if (ds->ime_active) {
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_active = 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++) {
Ref<InputEventKey> ev;
ev.instantiate();
ev->set_echo(false);
ev->set_keycode(Key::NONE);
ev->set_physical_keycode(Key::NONE);
ev->set_key_label(Key::NONE);
ev->set_unicode(text[i]);
ev->set_pressed(true);

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++] = ev;
}
}
} 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::DisplayServerWeb::ime_get_selection() const {
return ime_selection;
}

String DisplayServerWeb::DisplayServerWeb::ime_get_text() const {
return ime_text;
}

void DisplayServerWeb::process_joypads() {
Input *input = Input::get_singleton();
int32_t pads = godot_js_input_gamepad_sample_count();
Expand Down Expand Up @@ -1003,6 +1103,7 @@ DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode
godot_js_input_paste_cb(&DisplayServerWeb::update_clipboard_callback);
godot_js_input_drop_files_cb(&DisplayServerWeb::drop_files_js_callback);
godot_js_input_gamepad_cb(&DisplayServerWeb::gamepad_callback);
godot_js_set_ime_cb(&DisplayServerWeb::ime_callback);

// JS Display interface (js/libs/library_godot_display.js)
godot_js_display_fullscreen_cb(&DisplayServerWeb::fullscreen_change_callback);
Expand Down Expand Up @@ -1030,7 +1131,7 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
switch (p_feature) {
//case FEATURE_GLOBAL_MENU:
//case FEATURE_HIDPI:
//case FEATURE_IME:
case FEATURE_IME:
case FEATURE_ICON:
case FEATURE_CLIPBOARD:
case FEATURE_CURSOR_SHAPE:
Expand Down Expand Up @@ -1263,6 +1364,11 @@ void DisplayServerWeb::process_events() {
Input::get_singleton()->flush_buffered_events();
if (godot_js_input_gamepad_sample() == OK) {
process_joypads();
for (int i = 0; i < key_event_pos; i++) {
const Ref<InputEventKey> &ke = key_event_buffer[i];
Input::get_singleton()->parse_input_event(ke);
}
key_event_pos = 0;
}
}

Expand Down
15 changes: 15 additions & 0 deletions platform/web/display_server_web.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ class DisplayServerWeb : public DisplayServer {
uint64_t last_click_ms = 0;
MouseButton last_click_button_index = MouseButton::NONE;

bool ime_active = false;
String ime_text;
Vector2i ime_selection;
Vector<Ref<InputEventKey>> key_event_buffer;
int key_event_pos = 0;

bool swap_cancel_ok = false;
bool tts = false;

Expand All @@ -108,6 +114,8 @@ class DisplayServerWeb : public DisplayServer {
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);
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();
static void _request_quit_callback();
WASM_EXPORT static void window_blur_callback();
Expand Down Expand Up @@ -162,6 +170,13 @@ class DisplayServerWeb : public DisplayServer {
virtual MouseMode mouse_get_mode() 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
virtual bool is_touchscreen_available() const override;

Expand Down
4 changes: 4 additions & 0 deletions platform/web/godot_js.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ 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_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));

// 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();
Expand Down
104 changes: 103 additions & 1 deletion platform/web/js/libs/library_godot_input.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,86 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

/*
* IME API helper.
*/

const GodotIME = {
$GodotIME__deps: ['$GodotRuntime', '$GodotEventListeners'],
$GodotIME: {
ime: null,

ime_active: function (active) {
function focus_timer() {
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();
}
}
},

ime_position: function (x, y) {
if (GodotIME.ime) {
GodotIME.ime.style.left = `${x}px`;
GodotIME.ime.style.top = `${y}px`;
}
},

init: function (callback) {
function ime_event_cb(event) {
if (GodotIME.ime) {
if (event.type === 'compositionstart') {
callback(0, null);
GodotIME.ime.innerHTML = '';
} else if (event.type === 'compositionupdate') {
const ptr = GodotRuntime.allocString(event.data);
callback(1, ptr);
GodotRuntime.free(ptr);
} else if (event.type === 'compositionend') {
const ptr = GodotRuntime.allocString(event.data);
callback(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 = 'absolute';
ime.style.left = '0px';
ime.style.top = '0px';
ime.style.width = '2px';
ime.style.height = '2px';
ime.style.display = 'none';
ime.id = 'ime';
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);

ime.onblur = function () {
this.style.display = 'none';
GodotConfig.canvas.focus();
};

GodotConfig.canvas.appendChild(ime);
GodotIME.ime = ime;
},
},
};
mergeInto(LibraryManager.library, GodotIME);

/*
* Gamepad API helper.
*/
Expand Down Expand Up @@ -338,7 +418,7 @@ mergeInto(LibraryManager.library, GodotInputDragDrop);
* Godot exposed input functions.
*/
const GodotInput = {
$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop'],
$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop', '$GodotIME'],
$GodotInput: {
getModifiers: function (evt) {
return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);
Expand Down Expand Up @@ -461,6 +541,28 @@ const GodotInput = {
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: 'vi',
godot_js_set_ime_cb: function (p_cb) {
const ime_cb = GodotRuntime.get_func(p_cb);
GodotIME.init(ime_cb);
},

/*
* Gamepad API
*/
Expand Down

0 comments on commit 941ffe0

Please sign in to comment.