shadPS4/src/core/libraries/ime/ime_ui.cpp
Valdis Bogdāns db9921baf2
Some checks failed
Build and Release / reuse (push) Has been cancelled
Build and Release / clang-format (push) Has been cancelled
Build and Release / get-info (push) Has been cancelled
Build and Release / windows-sdl (push) Has been cancelled
Build and Release / windows-qt (push) Has been cancelled
Build and Release / macos-sdl (push) Has been cancelled
Build and Release / macos-qt (push) Has been cancelled
Build and Release / linux-sdl (push) Has been cancelled
Build and Release / linux-qt (push) Has been cancelled
Build and Release / linux-sdl-gcc (push) Has been cancelled
Build and Release / linux-qt-gcc (push) Has been cancelled
Build and Release / pre-release (push) Has been cancelled
GE2: Fix IME text conversion length handling (#3735)
- Reserve an extra space for the terminating character, resolving an issue in GE2 where the last character did not appear when input reached the maximum length.

Co-authored-by: w1naenator <valdis.bogdans@hotmail.com>
2025-10-14 08:41:47 +03:00

360 lines
13 KiB
C++

// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#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<char16_t>::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<std::mutex> 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<char16_t*>(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<char16_t*>(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<u32>(std::char_traits<char16_t>::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<char16_t*>(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<char16_t*>(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<u32>(std::char_traits<char16_t>::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<const ImWchar*>(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<ImWchar*>(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<std::mutex> 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<ImeUi*>(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<char16_t*>(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<s32>(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<u32>(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<u32>(dir));
ui->state->SendEvent(&caret_step);
}
lastCaretPos = data->CursorPos;
}
return 0;
}
void ImeUi::Free() {
RemoveLayer(this);
}
}; // namespace Libraries::Ime