// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include "ime_ui.h" #include "imgui/imgui_std.h" namespace Libraries::Ime { using namespace ImGui; static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f}; ImeState::ImeState(const OrbisImeParam* param, const OrbisImeParamExtended* extended) { if (!param) { LOG_ERROR(Lib_Ime, "Invalid IME parameters"); return; } if (!param->work) { LOG_ERROR(Lib_Ime, "Invalid work buffer pointer"); return; } if (!param->inputTextBuffer) { LOG_ERROR(Lib_Ime, "Invalid text buffer pointer"); return; } work_buffer = param->work; text_buffer = param->inputTextBuffer; // Respect both the absolute IME limit and the caller-provided limit max_text_length = std::min(param->maxTextLength, ORBIS_IME_MAX_TEXT_LENGTH); if (extended) { LOG_INFO(Lib_Ime, "Extended IME parameters provided"); } if (text_buffer) { const std::size_t text_len = std::char_traits::length(text_buffer); if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(), ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1)) { LOG_ERROR(Lib_Ime, "Failed to convert text to utf8 encoding"); } } } ImeState::ImeState(ImeState&& other) noexcept : work_buffer(other.work_buffer), text_buffer(other.text_buffer), max_text_length(other.max_text_length), current_text(std::move(other.current_text)), event_queue(std::move(other.event_queue)) { other.text_buffer = nullptr; other.max_text_length = 0; } ImeState& ImeState::operator=(ImeState&& other) noexcept { if (this != &other) { work_buffer = other.work_buffer; text_buffer = other.text_buffer; max_text_length = other.max_text_length; current_text = std::move(other.current_text); event_queue = std::move(other.event_queue); other.text_buffer = nullptr; other.max_text_length = 0; } return *this; } void ImeState::SendEvent(OrbisImeEvent* event) { std::unique_lock lock{queue_mutex}; event_queue.push(*event); } void ImeState::SendEnterEvent() { OrbisImeEvent enterEvent{}; enterEvent.id = OrbisImeEventId::PressEnter; // Include current text payload for consumers expecting text with Enter OrbisImeEditText text{}; text.str = reinterpret_cast(work_buffer); // Sync work and input buffers with the latest UTF-8 text if (current_text.begin()) { ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), reinterpret_cast(work_buffer), max_text_length + 1); if (text_buffer) { ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer, max_text_length + 1); } } if (text.str) { const u32 len = static_cast(std::char_traits::length(text.str)); // 0-based caret at end text.caret_index = len; text.area_num = 1; text.text_area[0].mode = OrbisImeTextAreaMode::Edit; // No edit happening on Enter: length=0; index can be caret text.text_area[0].index = len; text.text_area[0].length = 0; enterEvent.param.text = text; } LOG_DEBUG(Lib_Ime, "IME Event queued: PressEnter caret={} area_num={} edit.index={} edit.length={}", text.caret_index, text.area_num, text.text_area[0].index, text.text_area[0].length); SendEvent(&enterEvent); } void ImeState::SendCloseEvent() { OrbisImeEvent closeEvent{}; closeEvent.id = OrbisImeEventId::PressClose; // Populate text payload with current buffer snapshot OrbisImeEditText text{}; text.str = reinterpret_cast(work_buffer); // Sync work and input buffers with the latest UTF-8 text if (current_text.begin()) { ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), reinterpret_cast(work_buffer), max_text_length + 1); if (text_buffer) { ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer, max_text_length + 1); } } if (text.str) { const u32 len = static_cast(std::char_traits::length(text.str)); // 0-based caret at end text.caret_index = len; text.area_num = 1; text.text_area[0].mode = OrbisImeTextAreaMode::Edit; // No edit happening on Close: length=0; index can be caret text.text_area[0].index = len; text.text_area[0].length = 0; closeEvent.param.text = text; } LOG_DEBUG(Lib_Ime, "IME Event queued: PressClose caret={} area_num={} edit.index={} edit.length={}", text.caret_index, text.area_num, text.text_area[0].index, text.text_area[0].length); SendEvent(&closeEvent); } void ImeState::SetText(const char16_t* text, u32 length) { if (!text) { LOG_WARNING(Lib_Ime, "ImeState::SetText received null text pointer"); return; } // Clamp to the effective maximum number of characters const u32 clamped_len = std::min(length, max_text_length) + 1; if (!ConvertOrbisToUTF8(text, clamped_len, current_text.begin(), current_text.capacity())) { LOG_ERROR(Lib_Ime, "ImeState::SetText failed to convert updated text to UTF-8"); return; } } void ImeState::SetCaret(u32 position) {} bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len, char* utf8_text, std::size_t utf8_text_len) { std::fill(utf8_text, utf8_text + utf8_text_len, '\0'); const ImWchar* orbis_text_ptr = reinterpret_cast(orbis_text); ImTextStrToUtf8(utf8_text, utf8_text_len, orbis_text_ptr, orbis_text_ptr + orbis_text_len); return true; } bool ImeState::ConvertUTF8ToOrbis(const char* utf8_text, std::size_t utf8_text_len, char16_t* orbis_text, std::size_t orbis_text_len) { std::fill(orbis_text, orbis_text + orbis_text_len, u'\0'); const char* end = utf8_text ? (utf8_text + utf8_text_len) : nullptr; ImTextStrFromUtf8(reinterpret_cast(orbis_text), orbis_text_len, utf8_text, end); return true; } ImeUi::ImeUi(ImeState* state, const OrbisImeParam* param, const OrbisImeParamExtended* extended) : state(state), ime_param(param), extended_param(extended) { if (param) { AddLayer(this); } } ImeUi::~ImeUi() { std::scoped_lock lock(draw_mutex); Free(); } ImeUi& ImeUi::operator=(ImeUi&& other) { std::scoped_lock lock(draw_mutex, other.draw_mutex); Free(); state = other.state; ime_param = other.ime_param; first_render = other.first_render; other.state = nullptr; other.ime_param = nullptr; AddLayer(this); return *this; } void ImeUi::Draw() { std::unique_lock lock{draw_mutex}; if (!state) { return; } const auto& ctx = *GetCurrentContext(); const auto& io = ctx.IO; // TODO: Figure out how to properly translate the positions - // for example, if a game wants to center the IME panel, // we have to translate the panel position in a way that it // still becomes centered, as the game normally calculates // the position assuming a it's running on a 1920x1080 screen, // whereas we are running on a 1280x720 window size (by default). // // e.g. Panel position calculation from a game: // param.posx = (1920 / 2) - (panelWidth / 2); // param.posy = (1080 / 2) - (panelHeight / 2); const auto size = GetIO().DisplaySize; f32 pos_x = (ime_param->posx / 1920.0f * (float)size.x); f32 pos_y = (ime_param->posy / 1080.0f * (float)size.y); ImVec2 window_pos = {pos_x, pos_y}; ImVec2 window_size = {500.0f, 100.0f}; // SetNextWindowPos(window_pos); SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); SetNextWindowSize(window_size); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { SetNextWindowFocus(); } if (Begin("IME##Ime", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings)) { DrawPrettyBackground(); DrawInputText(); SetCursorPosY(GetCursorPosY() + 10.0f); const char* button_text; button_text = "Done##ImeDone"; float button_spacing = 10.0f; float total_button_width = BUTTON_SIZE.x * 2 + button_spacing; float button_start_pos = (window_size.x - total_button_width) / 2.0f; SetCursorPosX(button_start_pos); if (Button(button_text, BUTTON_SIZE) || (IsKeyPressed(ImGuiKey_Enter))) { state->SendEnterEvent(); } SameLine(0.0f, button_spacing); if (Button("Close##ImeClose", BUTTON_SIZE)) { state->SendCloseEvent(); } } End(); first_render = false; } void ImeUi::DrawInputText() { ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f}; SetCursorPosX(20.0f); if (first_render) { SetKeyboardFocusHere(); } if (InputTextExLimited("##ImeInput", nullptr, state->current_text.begin(), ime_param->maxTextLength * 4 + 1, input_size, ImGuiInputTextFlags_CallbackAlways, ime_param->maxTextLength, InputTextCallback, this)) { } } int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ImeUi* ui = static_cast(data->UserData); ASSERT(ui); static std::string lastText; static int lastCaretPos = -1; std::string currentText(data->Buf, data->BufTextLen); if (currentText != lastText) { OrbisImeEditText eventParam{}; eventParam.str = reinterpret_cast(ui->ime_param->work); eventParam.area_num = 1; eventParam.text_area[0].mode = OrbisImeTextAreaMode::Edit; if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str, ui->state->max_text_length + 1)) { LOG_ERROR(Lib_Ime, "Failed to convert UTF-8 to Orbis for eventParam.str"); return 0; } if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, ui->ime_param->inputTextBuffer, ui->state->max_text_length + 1)) { LOG_ERROR(Lib_Ime, "Failed to convert UTF-8 to Orbis for inputTextBuffer"); return 0; } eventParam.caret_index = data->CursorPos; eventParam.text_area[0].index = data->CursorPos; eventParam.text_area[0].length = (data->CursorPos > lastCaretPos) ? 1 : -1; // data->CursorPos; OrbisImeEvent event{}; event.id = OrbisImeEventId::UpdateText; event.param.text = eventParam; LOG_DEBUG(Lib_Ime, "IME Event queued: UpdateText(type, " "delete)\neventParam.caret_index={}\narea_num={}\neventParam.text_area[0].mode={}" "\neventParam.text_area[0].index={}\neventParam.text_area[0].length={}", eventParam.caret_index, eventParam.area_num, static_cast(eventParam.text_area[0].mode), eventParam.text_area[0].index, eventParam.text_area[0].length); lastText = currentText; lastCaretPos = -1; ui->state->SendEvent(&event); } if (lastCaretPos == -1) { lastCaretPos = data->CursorPos; } else if (data->CursorPos != lastCaretPos) { const int delta = data->CursorPos - lastCaretPos; // Emit one UpdateCaret per delta step (delta may be ±1 or a jump) const bool move_right = delta > 0; const u32 steps = static_cast(std::abs(delta)); OrbisImeCaretMovementDirection dir = move_right ? OrbisImeCaretMovementDirection::Right : OrbisImeCaretMovementDirection::Left; for (u32 i = 0; i < steps; ++i) { OrbisImeEvent caret_step{}; caret_step.id = OrbisImeEventId::UpdateCaret; caret_step.param.caret_move = dir; LOG_DEBUG(Lib_Ime, "IME Event queued: UpdateCaret(step {}/{}), dir={}", i + 1, steps, static_cast(dir)); ui->state->SendEvent(&caret_step); } lastCaretPos = data->CursorPos; } return 0; } void ImeUi::Free() { RemoveLayer(this); } }; // namespace Libraries::Ime