Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Web] Add IME input support #79362

Merged
merged 1 commit into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 140 additions & 13 deletions platform/web/display_server_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
OS_Web::get_singleton()->resume_audio();

DisplayServerWeb *ds = get_singleton();
if (ds->ime_started) {
return;
}

char32_t c = 0x00;
String unicode = p_key_event_key;
if (unicode.length() == 1) {
Expand All @@ -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 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.
Input::get_singleton()->flush_buffered_events();
Expand Down Expand Up @@ -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) {
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) {
godot_js_display_canvas_focus();
} else {
Expand Down Expand Up @@ -726,7 +735,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 +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() {
Input *input = Input::get_singleton();
int32_t pads = godot_js_input_gamepad_sample_count();
Expand Down Expand Up @@ -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) {
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()) {
Variant event = int(p_notification);
ds->window_event_callback.call(event);
Expand Down Expand Up @@ -1003,6 +1109,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, &DisplayServerWeb::key_callback, key_event.code, key_event.key);

// JS Display interface (js/libs/library_godot_display.js)
godot_js_display_fullscreen_cb(&DisplayServerWeb::fullscreen_change_callback);
Expand Down Expand Up @@ -1030,7 +1137,6 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
switch (p_feature) {
//case FEATURE_GLOBAL_MENU:
//case FEATURE_HIDPI:
//case FEATURE_IME:
case FEATURE_ICON:
case FEATURE_CLIPBOARD:
case FEATURE_CURSOR_SHAPE:
Expand All @@ -1044,6 +1150,9 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
//case FEATURE_WINDOW_TRANSPARENCY:
//case FEATURE_KEEP_SCREEN_ON:
//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:
return godot_js_display_vk_available() != 0;
case FEATURE_TEXT_TO_SPEECH:
Expand Down Expand Up @@ -1263,6 +1372,24 @@ 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 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;
}
}

Expand Down
28 changes: 28 additions & 0 deletions platform/web/display_server_web.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@ class DisplayServerWeb : public DisplayServer {
uint64_t last_click_ms = 0;
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 tts = false;

Expand All @@ -108,6 +127,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 +183,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
5 changes: 5 additions & 0 deletions platform/web/godot_js.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_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
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
Loading
Loading