diff --git a/src/core/libraries/ime/ime.cpp b/src/core/libraries/ime/ime.cpp index a2527be02..258cc61e1 100644 --- a/src/core/libraries/ime/ime.cpp +++ b/src/core/libraries/ime/ime.cpp @@ -1,10 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include -#include #include #include "common/logging/log.h" #include "core/libraries/ime/ime.h" @@ -21,83 +17,47 @@ static ImeUi g_ime_ui; class ImeHandler { public: - ImeHandler(const OrbisImeKeyboardParam* param, - Libraries::UserService::OrbisUserServiceUserId userId) - : m_user_id(userId) { - LOG_DEBUG(Lib_Ime, "Creating ImeHandler for keyboard (user {})", userId); - LOG_DEBUG(Lib_Ime, "Keyboard constructor received param_ptr={:p}", - static_cast(param)); - if (!param) { - LOG_ERROR(Lib_Ime, "ImeHandler keyboard constructor received null param"); - return; - } - Init(param, false); + ImeHandler(const OrbisImeKeyboardParam* param) { + Init(param, nullptr, false); } ImeHandler(const OrbisImeParam* param, const OrbisImeParamExtended* extended = nullptr) { - LOG_DEBUG(Lib_Ime, "Creating ImeHandler for IME"); - LOG_DEBUG(Lib_Ime, "IME constructor received param_ptr={:p} extended_ptr={:p}", - static_cast(param), static_cast(extended)); - if (!param) { - LOG_ERROR(Lib_Ime, "ImeHandler IME constructor received null param"); - return; - } - if (extended) { - m_extended = *extended; - m_has_extended = true; - } - Init(param, true); + + LOG_DEBUG(Lib_Ime, "param->work: 0x{:X}", + static_cast(reinterpret_cast(param->work))); + LOG_DEBUG(Lib_Ime, "param->inputTextBuffer: 0x{:X}", + static_cast(reinterpret_cast(param->arg))); + Init(param, extended, true); } ~ImeHandler() = default; - void Init(const void* param, bool ime_mode) { - LOG_DEBUG(Lib_Ime, "ImeHandler::Init begin (ime_mode={}, param_ptr={:p})", ime_mode, param); - + void Init(const void* param, const OrbisImeParamExtended* extended, bool ime_mode) { if (ime_mode) { - LOG_DEBUG(Lib_Ime, "Copying OrbisImeParam from guest"); m_param.ime = *(OrbisImeParam*)param; + LOG_DEBUG(Lib_Ime, "m_param.ime.work: 0x{:X}", + static_cast(reinterpret_cast(m_param.ime.work))); + LOG_DEBUG(Lib_Ime, "m_param.ime.inputTextBuffer: 0x{:X}", + static_cast(reinterpret_cast(m_param.ime.inputTextBuffer))); + if (extended) + m_param.ime_ext = *extended; + else + m_param.ime_ext = {}; } else { - LOG_DEBUG(Lib_Ime, "Copying OrbisImeKeyboardParam from guest"); m_param.key = *(OrbisImeKeyboardParam*)param; } m_ime_mode = ime_mode; - LOG_DEBUG(Lib_Ime, "Ime mode flag set to {}", ime_mode); // Open an event to let the game know the IME has started OrbisImeEvent openEvent{}; openEvent.id = (ime_mode ? OrbisImeEventId::Open : OrbisImeEventId::KeyboardOpen); - LOG_DEBUG(Lib_Ime, "Prepared initial event with id {}", static_cast(openEvent.id)); if (ime_mode) { - LOG_DEBUG(Lib_Ime, "calling sceImeGetPanelSize"); - Error e = sceImeGetPanelSize(&m_param.ime, &openEvent.param.rect.width, - &openEvent.param.rect.height); - if (e != Error::OK) { - LOG_ERROR(Lib_Ime, "sceImeGetPanelSize returned 0x{:X}", static_cast(e)); - } else { - LOG_DEBUG(Lib_Ime, "Panel size width={} height={}", openEvent.param.rect.width, - openEvent.param.rect.height); - } - + sceImeGetPanelSize(&m_param.ime, &openEvent.param.rect.width, + &openEvent.param.rect.height); openEvent.param.rect.x = m_param.ime.posx; openEvent.param.rect.y = m_param.ime.posy; - LOG_DEBUG(Lib_Ime, "Panel position set to ({}, {})", openEvent.param.rect.x, - openEvent.param.rect.y); } else { - LOG_DEBUG(Lib_Ime, "Initializing keyboard handler for user {}", m_user_id); - - // Report the real PS4 user ID, and mark “no physical keyboard” so games know - openEvent.param.resource_id_array.user_id = m_user_id; - for (auto& rid : openEvent.param.resource_id_array.resource_id) { - rid = -1; - } - LOG_DEBUG(Lib_Ime, "Reporting virtual keyboard resource ids initialized to -1"); - LOG_DEBUG(Lib_Ime, "Dispatching initial keyboard event to handler 0x{:X} (arg={:p})", - static_cast(reinterpret_cast(m_param.key.handler)), - static_cast(m_param.key.arg)); - // The guest handler expects PS4 address space; skip dispatch until we provide a proper - // translation. - LOG_WARNING(Lib_Ime, "Skipping initial keyboard event dispatch (guest handler requires " - "guest-accessible payload)"); + openEvent.param.resource_id_array.user_id = 1; + openEvent.param.resource_id_array.resource_id[0] = 1; } // Are we supposed to call the event handler on init with @@ -107,13 +67,15 @@ public: }*/ if (ime_mode) { - LOG_DEBUG(Lib_Ime, "Creating IME state and UI instances"); - g_ime_state = ImeState(&m_param.ime, m_has_extended ? &m_extended : nullptr); - g_ime_ui = ImeUi(&g_ime_state, &m_param.ime, m_has_extended ? &m_extended : nullptr); - LOG_DEBUG(Lib_Ime, "ImeState and ImeUi initialized"); - } + g_ime_state = ImeState(&m_param.ime, &m_param.ime_ext); + g_ime_ui = ImeUi(&g_ime_state, &m_param.ime, &m_param.ime_ext); - LOG_DEBUG(Lib_Ime, "ImeHandler::Init complete"); + // Queue the Open event so it is delivered on next sceImeUpdate + LOG_DEBUG(Lib_Ime, "IME Event queued: Open rect x={}, y={}, w={}, h={}", + openEvent.param.rect.x, openEvent.param.rect.y, openEvent.param.rect.width, + openEvent.param.rect.height); + g_ime_state.SendEvent(&openEvent); + } } Error Update(OrbisImeEventHandler handler) { @@ -127,13 +89,6 @@ public: while (!g_ime_state.event_queue.empty()) { OrbisImeEvent event = g_ime_state.event_queue.front(); g_ime_state.event_queue.pop(); - - const auto event_id = event.id; - const auto event_name = magic_enum::enum_name(event_id); - const char* event_name_cstr = event_name.empty() ? "Unknown" : event_name.data(); - LOG_DEBUG(Lib_Ime, "ImeHandler::Update dispatching event_id={} ({}) remaining_queue={}", - static_cast(event_id), event_name_cstr, g_ime_state.event_queue.size()); - Execute(handler, &event, false); } @@ -141,17 +96,6 @@ public: } void Execute(OrbisImeEventHandler handler, OrbisImeEvent* event, bool use_param_handler) { - const auto param_arg = m_ime_mode ? static_cast(m_param.ime.arg) - : static_cast(m_param.key.arg); - const OrbisImeEventHandler target_handler = - use_param_handler ? (m_ime_mode ? m_param.ime.handler : m_param.key.handler) : handler; - LOG_DEBUG(Lib_Ime, - "Execute (ime_mode={}, use_param_handler={}, target_handler=0x{:X}, " - "param_arg={:p}, event={:p}, event_id={})", - m_ime_mode, use_param_handler, - static_cast(reinterpret_cast(target_handler)), param_arg, - static_cast(event), event ? static_cast(event->id) : -1); - if (m_ime_mode) { OrbisImeParam param = m_param.ime; if (use_param_handler) { @@ -174,34 +118,7 @@ public: LOG_WARNING(Lib_Ime, "ImeHandler::SetText received null text pointer"); return Error::INVALID_ADDRESS; } - - std::size_t requested_length = length; - if (requested_length == 0) { - requested_length = std::char_traits::length(text); - } - - constexpr std::size_t kMaxOrbisLength = ORBIS_IME_MAX_TEXT_LENGTH; - const std::size_t effective_length = - std::min(requested_length, kMaxOrbisLength); - - std::array utf8_buffer{}; - std::string preview; - if (g_ime_state.ConvertOrbisToUTF8(text, effective_length, utf8_buffer.data(), - utf8_buffer.size())) { - std::string_view utf8_view{utf8_buffer.data()}; - constexpr std::size_t kPreviewLength = 64; - preview = std::string{utf8_view.substr(0, std::min(kPreviewLength, utf8_view.size()))}; - if (utf8_view.size() > kPreviewLength) { - preview += "..."; - } - } else { - preview = ""; - } - - LOG_DEBUG(Lib_Ime, "ImeHandler::SetText game feedback length={} (effective={}) preview={}", - length, effective_length, preview); - - g_ime_state.SetText(text, static_cast(effective_length)); + g_ime_state.SetText(text, length); return Error::OK; } @@ -210,10 +127,6 @@ public: LOG_WARNING(Lib_Ime, "ImeHandler::SetCaret received null caret pointer"); return Error::INVALID_ADDRESS; } - - LOG_DEBUG(Lib_Ime, "ImeHandler::SetCaret game feedback index={} pos=({}, {}) height={}", - caret->index, caret->x, caret->y, caret->height); - g_ime_state.SetCaret(caret->index); return Error::OK; } @@ -223,15 +136,12 @@ public: } private: - Libraries::UserService::OrbisUserServiceUserId m_user_id{}; - union ImeParam { + struct ImeParam { OrbisImeKeyboardParam key; OrbisImeParam ime; + OrbisImeParamExtended ime_ext; } m_param{}; bool m_ime_mode = false; - - OrbisImeParamExtended m_extended{}; - bool m_has_extended = false; }; static std::unique_ptr g_ime_handler; @@ -348,7 +258,7 @@ int PS4_SYSV_ABI sceImeGetPanelPositionAndForm() { } Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* height) { - LOG_DEBUG(Lib_Ime, "sceImeGetPanelSize called"); + LOG_INFO(Lib_Ime, "called"); if (!param) { LOG_ERROR(Lib_Ime, "Invalid param: NULL"); @@ -406,7 +316,7 @@ Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u3 } Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceUserId userId) { - LOG_DEBUG(Lib_Ime, "called"); + LOG_INFO(Lib_Ime, "called"); if (!g_keyboard_handler) { LOG_ERROR(Lib_Ime, "No keyboard handler is open"); @@ -419,7 +329,7 @@ Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceU return Error::INVALID_USER_ID; } - g_keyboard_handler.release(); + g_keyboard_handler.reset(); if (g_keyboard_handler) { LOG_ERROR(Lib_Ime, "failed to close keyboard handler, it is still open"); return Error::INTERNAL; @@ -436,7 +346,7 @@ int PS4_SYSV_ABI sceImeKeyboardGetInfo() { Error PS4_SYSV_ABI sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userId, OrbisImeKeyboardResourceIdArray* resourceIdArray) { - LOG_DEBUG(Lib_Ime, "(partial) called"); + LOG_INFO(Lib_Ime, "(partial) called"); if (!resourceIdArray) { LOG_ERROR(Lib_Ime, "Invalid resourceIdArray: NULL"); @@ -446,10 +356,12 @@ sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userI // TODO: Check for valid user IDs. if (userId == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { LOG_ERROR(Lib_Ime, "Invalid userId: {}", userId); + /* resourceIdArray->user_id = userId; for (u32& id : resourceIdArray->resource_id) { id = 0; } + */ return Error::INVALID_USER_ID; } @@ -476,7 +388,7 @@ sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userI Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUserId userId, const OrbisImeKeyboardParam* param) { - LOG_DEBUG(Lib_Ime, "called"); + LOG_INFO(Lib_Ime, "called"); if (!param) { LOG_ERROR(Lib_Ime, "Invalid param: NULL"); return Error::INVALID_ADDRESS; @@ -531,11 +443,13 @@ Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUs LOG_ERROR(Lib_Ime, "USB keyboard some special kind of failure"); return Error::CONNECTION_FAILED; } + if (g_keyboard_handler) { LOG_ERROR(Lib_Ime, "Keyboard handler is already open"); return Error::BUSY; } - g_keyboard_handler = std::make_unique(param, userId); + + g_keyboard_handler = std::make_unique(param); if (!g_keyboard_handler) { LOG_ERROR(Lib_Ime, "Failed to create keyboard handler"); return Error::INTERNAL; // or Error::NO_MEMORY; @@ -560,7 +474,7 @@ int PS4_SYSV_ABI sceImeKeyboardUpdate() { } Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExtended* extended) { - LOG_DEBUG(Lib_Ime, "called"); + LOG_INFO(Lib_Ime, "called"); if (!param) { LOG_ERROR(Lib_Ime, "Invalid param: NULL"); @@ -715,13 +629,13 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt // Todo: validate arg if (false) { - LOG_ERROR(Lib_Ime, "Invalid arg: NULL"); + LOG_ERROR(Lib_Ime, "Invalid arg"); return Error::INVALID_ARG; } // Todo: validate handler if (false) { - LOG_ERROR(Lib_Ime, "Invalid handler: NULL"); + LOG_ERROR(Lib_Ime, "Invalid handler"); return Error::INVALID_HANDLER; } @@ -737,7 +651,11 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt return Error::BUSY; } - g_ime_handler = std::make_unique(param, extended); + if (extended) { + g_ime_handler = std::make_unique(param, extended); + } else { + g_ime_handler = std::make_unique(param); + } if (!g_ime_handler) { LOG_ERROR(Lib_Ime, "Failed to create IME handler"); return Error::NO_MEMORY; // or Error::INTERNAL @@ -753,14 +671,14 @@ int PS4_SYSV_ABI sceImeOpenInternal() { } void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param) { - LOG_DEBUG(Lib_Ime, "sceImeParamInit called"); + LOG_INFO(Lib_Ime, "called"); if (!param) { return; } memset(param, 0, sizeof(OrbisImeParam)); - param->user_id = -1; + param->user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; } int PS4_SYSV_ABI sceImeSetCandidateIndex() { @@ -811,7 +729,7 @@ Error PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler) { } if (!g_ime_handler && !g_keyboard_handler) { - LOG_TRACE(Lib_Ime, "sceImeUpdate called with no active handler"); + LOG_ERROR(Lib_Ime, "sceImeUpdate called with no active handler"); return Error::OK; } diff --git a/src/core/libraries/ime/ime_ui.cpp b/src/core/libraries/ime/ime_ui.cpp index eb97d85d4..0ea56a106 100644 --- a/src/core/libraries/ime/ime_ui.cpp +++ b/src/core/libraries/ime/ime_ui.cpp @@ -1,15 +1,11 @@ // 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" -#include -#include -#include - -#include "common/logging/log.h" - namespace Libraries::Ime { using namespace ImGui; @@ -18,23 +14,31 @@ 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; - max_text_length = param->maxTextLength; + // 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) { - extended_param = *extended; - has_extended = true; + 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)) { - LOG_ERROR(Lib_ImeDialog, "Failed to convert text to utf8 encoding"); + ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1)) { + LOG_ERROR(Lib_Ime, "Failed to convert text to utf8 encoding"); } } } @@ -62,36 +66,75 @@ ImeState& ImeState::operator=(ImeState&& other) noexcept { } void ImeState::SendEvent(OrbisImeEvent* event) { - if (!event) { - LOG_WARNING(Lib_Ime, "ImeState::SendEvent called with null event"); - return; - } - - const auto event_id = event->id; - const auto event_name = magic_enum::enum_name(event_id); - const char* event_name_cstr = event_name.empty() ? "Unknown" : event_name.data(); - std::unique_lock lock{queue_mutex}; event_queue.push(*event); - - LOG_DEBUG(Lib_Ime, "ImeState queued event_id={} ({}) queue_size={}", static_cast(event_id), - event_name_cstr, event_queue.size()); } void ImeState::SendEnterEvent() { - LOG_DEBUG(Lib_Ime, "ImeState::SendEnterEvent triggered"); - 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); + if (text_buffer) { + ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer, + max_text_length); + } + } + 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() { - LOG_DEBUG(Lib_Ime, "ImeState::SendCloseEvent triggered (work_buffer={:p})", work_buffer); - OrbisImeEvent closeEvent{}; closeEvent.id = OrbisImeEventId::PressClose; - closeEvent.param.text.str = reinterpret_cast(work_buffer); + + // 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); + if (text_buffer) { + ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer, + max_text_length); + } + } + 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); } @@ -101,46 +144,14 @@ void ImeState::SetText(const char16_t* text, u32 length) { return; } - std::size_t requested_length = length; - if (requested_length == 0) { - requested_length = std::char_traits::length(text); - } - - const std::size_t buffer_capacity = - max_text_length != 0 ? max_text_length : ORBIS_IME_MAX_TEXT_LENGTH; - const std::size_t effective_length = std::min(requested_length, buffer_capacity); - - if (text_buffer && buffer_capacity > 0) { - const std::size_t copy_length = std::min(effective_length, buffer_capacity); - std::copy_n(text, copy_length, text_buffer); - const std::size_t terminator_index = - copy_length < buffer_capacity ? copy_length : buffer_capacity - 1; - text_buffer[terminator_index] = u'\0'; - } - - if (!ConvertOrbisToUTF8(text, effective_length, current_text.begin(), - current_text.capacity())) { + // Clamp to the effective maximum number of characters + const u32 clamped_len = std::min(length, max_text_length); + 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; } - - const auto utf8_view = current_text.to_view(); - constexpr std::size_t kPreviewLength = 64; - std::string preview = - std::string{utf8_view.substr(0, std::min(kPreviewLength, utf8_view.size()))}; - if (utf8_view.size() > kPreviewLength) { - preview += "..."; - } - - LOG_DEBUG(Lib_Ime, "ImeState::SetText stored game feedback length={} preview={}", - effective_length, preview); -} - -void ImeState::SetCaret(u32 position) { - LOG_DEBUG(Lib_Ime, - "ImeState::SetCaret stored game feedback caret_index={} current_text_length={}", - position, current_text.size()); } +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) { @@ -154,7 +165,8 @@ bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_ 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'); - ImTextStrFromUtf8(reinterpret_cast(orbis_text), orbis_text_len, utf8_text, nullptr); + const char* end = utf8_text ? (utf8_text + utf8_text_len) : nullptr; + ImTextStrFromUtf8(reinterpret_cast(orbis_text), orbis_text_len, utf8_text, end); return true; } @@ -260,9 +272,10 @@ void ImeUi::DrawInputText() { if (first_render) { SetKeyboardFocusHere(); } - if (InputTextEx("##ImeInput", nullptr, state->current_text.begin(), - ime_param->maxTextLength * 4 + 1, input_size, - ImGuiInputTextFlags_CallbackAlways, InputTextCallback, this)) { + if (InputTextExLimited("##ImeInput", nullptr, state->current_text.begin(), + ime_param->maxTextLength * 4 + 1, input_size, + ImGuiInputTextFlags_CallbackAlways, ime_param->maxTextLength, + InputTextCallback, this)) { } } @@ -271,69 +284,69 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { 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.caret_index = data->CursorPos; eventParam.area_num = 1; - eventParam.text_area[0].mode = OrbisImeTextAreaMode::Edit; - eventParam.text_area[0].index = data->CursorPos; - eventParam.text_area[0].length = data->BufTextLen; if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str, - ui->ime_param->maxTextLength)) { - LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); + ui->state->max_text_length)) { + 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->ime_param->maxTextLength)) { - LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); + ui->state->max_text_length)) { + 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; - - constexpr std::size_t kPreviewLength = 64; - std::string preview = currentText.substr(0, kPreviewLength); - if (currentText.size() > kPreviewLength) { - preview += "..."; - } - LOG_DEBUG(Lib_Ime, "ImeUi enqueuing UpdateText: caret_index={} length={} preview={}", - eventParam.caret_index, data->BufTextLen, preview); + 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); } - static int lastCaretPos = -1; if (lastCaretPos == -1) { lastCaretPos = data->CursorPos; } else if (data->CursorPos != lastCaretPos) { - OrbisImeCaretMovementDirection caretDirection = OrbisImeCaretMovementDirection::Still; - if (data->CursorPos < lastCaretPos) { - caretDirection = OrbisImeCaretMovementDirection::Left; - } else if (data->CursorPos > lastCaretPos) { - caretDirection = OrbisImeCaretMovementDirection::Right; + 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); } - OrbisImeEvent event{}; - event.id = OrbisImeEventId::UpdateCaret; - event.param.caret_move = caretDirection; - - const auto caret_direction_name = magic_enum::enum_name(caretDirection); - const char* caret_direction_cstr = - caret_direction_name.empty() ? "Unknown" : caret_direction_name.data(); - LOG_DEBUG(Lib_Ime, "ImeUi enqueuing UpdateCaret: caret_pos={} direction={} ({})", - data->CursorPos, static_cast(caretDirection), caret_direction_cstr); - lastCaretPos = data->CursorPos; - ui->state->SendEvent(&event); } return 0; diff --git a/src/core/libraries/ime/ime_ui.h b/src/core/libraries/ime/ime_ui.h index e94ce8ea8..5eb371bd9 100644 --- a/src/core/libraries/ime/ime_ui.h +++ b/src/core/libraries/ime/ime_ui.h @@ -26,9 +26,6 @@ class ImeState { char16_t* text_buffer{}; u32 max_text_length = 0; - OrbisImeParamExtended extended_param{}; - bool has_extended = false; - // A character can hold up to 4 bytes in UTF-8 Common::CString current_text; diff --git a/src/imgui/imgui_std.h b/src/imgui/imgui_std.h index 743702657..986a3e24f 100644 --- a/src/imgui/imgui_std.h +++ b/src/imgui/imgui_std.h @@ -88,4 +88,112 @@ static void DrawCenteredText(const char* text, const char* text_end = nullptr, SetCursorPos(pos + content); } +// Limited-length InputTextEx wrapper (limits UTF-8 code points) +// - max_chars counts Unicode code points, not bytes +// - Works for single-line and multi-line +// - Chains user callbacks if provided +struct InputTextLimitCtx { + int max_chars; + ImGuiInputTextCallback user_cb; + void* user_user_data; + ImGuiInputTextFlags forward_flags; // original flags requested by caller +}; + +inline int InputTextLimitCallback(ImGuiInputTextCallbackData* data) { + InputTextLimitCtx* ctx = static_cast(data->UserData); + if (ctx && ctx->user_cb) { + const ImGuiInputTextFlags ev = data->EventFlag; + const ImGuiInputTextFlags ff = ctx->forward_flags; + const bool should_forward = + ((ev == ImGuiInputTextFlags_CallbackAlways) && + (ff & ImGuiInputTextFlags_CallbackAlways)) || + ((ev == ImGuiInputTextFlags_CallbackEdit) && (ff & ImGuiInputTextFlags_CallbackEdit)) || + ((ev == ImGuiInputTextFlags_CallbackCharFilter) && + (ff & ImGuiInputTextFlags_CallbackCharFilter)) || + ((ev == ImGuiInputTextFlags_CallbackCompletion) && + (ff & ImGuiInputTextFlags_CallbackCompletion)) || + ((ev == ImGuiInputTextFlags_CallbackHistory) && + (ff & ImGuiInputTextFlags_CallbackHistory)) || + ((ev == ImGuiInputTextFlags_CallbackResize) && + (ff & ImGuiInputTextFlags_CallbackResize)); + if (should_forward) { + void* orig = data->UserData; + data->UserData = ctx->user_user_data; + int user_ret = ctx->user_cb(data); + data->UserData = orig; + if (user_ret != 0) { + return user_ret; + } + } + } + + if (!ctx || ctx->max_chars < 0) { + return 0; + } + + // Enforce limit: discard extra characters on filter, trim on edit + if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) { + ImGuiContext* g = data->Ctx; + if (!g) { + return 0; + } + ImGuiInputTextState* st = &g->InputTextState; + if (st == nullptr || st->TextSrc == nullptr) { + return 0; + } + int cur_chars = ImTextCountCharsFromUtf8(st->TextSrc, st->TextSrc + st->TextLen); + int sel_chars = 0; + if (st->HasSelection()) { + const int ib = st->GetSelectionStart(); + const int ie = st->GetSelectionEnd(); + sel_chars = ImTextCountCharsFromUtf8(st->TextSrc + ib, st->TextSrc + ie); + } + const int remaining = ctx->max_chars - (cur_chars - sel_chars); + if (remaining <= 0) { + data->EventChar = 0; + return 1; // discard + } + return 0; + } + + if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) { + // Trim tail to ensure text length <= max_chars code points + const char* s = data->Buf; + const char* end = s + data->BufTextLen; + const char* p = s; + int codepoints = 0; + while (p < end && codepoints < ctx->max_chars) { + unsigned int c; + int len = ImTextCharFromUtf8(&c, p, end); + if (len <= 0) + break; + p += len; + codepoints++; + } + if (p < end) { + const int keep_bytes = static_cast(p - s); + data->DeleteChars(keep_bytes, data->BufTextLen - keep_bytes); + if (data->CursorPos > data->BufTextLen) + data->CursorPos = data->BufTextLen; + if (data->SelectionStart > data->BufTextLen) + data->SelectionStart = data->BufTextLen; + if (data->SelectionEnd > data->BufTextLen) + data->SelectionEnd = data->BufTextLen; + } + return 0; + } + + return 0; +} + +inline bool InputTextExLimited(const char* label, const char* hint, char* buf, int buf_size, + const ImVec2& size_arg, ImGuiInputTextFlags flags, int max_chars, + ImGuiInputTextCallback callback = nullptr, + void* user_data = nullptr) { + InputTextLimitCtx ctx{max_chars, callback, user_data, flags}; + ImGuiInputTextFlags flags2 = + flags | ImGuiInputTextFlags_CallbackCharFilter | ImGuiInputTextFlags_CallbackEdit; + return InputTextEx(label, hint, buf, buf_size, size_arg, flags2, InputTextLimitCallback, &ctx); +} + } // namespace ImGui