This commit is contained in:
Valdis Bogdāns 2026-03-31 14:56:41 +08:00 committed by GitHub
commit 35cc949dcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2207 additions and 491 deletions

View File

@ -518,6 +518,8 @@ set(HLE_LIBC_INTERNAL_LIB src/core/libraries/libc_internal/libc_internal.cpp
set(IME_LIB src/core/libraries/ime/error_dialog.cpp
src/core/libraries/ime/error_dialog.h
src/core/libraries/ime/ime_common.h
src/core/libraries/ime/ime_kb_layout.cpp
src/core/libraries/ime/ime_kb_layout.h
src/core/libraries/ime/ime_dialog_ui.cpp
src/core/libraries/ime/ime_dialog_ui.h
src/core/libraries/ime/ime_dialog.cpp

View File

@ -159,6 +159,8 @@ static ConfigEntry<bool> isMotionControlsEnabled(true);
static ConfigEntry<bool> useUnifiedInputConfig(true);
static ConfigEntry<string> defaultControllerID("");
static ConfigEntry<bool> backgroundControllerInput(false);
static ConfigEntry<bool> imeAccessibilityEnabled(false);
static ConfigEntry<bool> imeUrlMailShortPanel(false);
// Audio
static ConfigEntry<string> micDevice("Default Device");
@ -832,6 +834,22 @@ void setBackgroundControllerInput(bool enable, bool is_game_specific) {
backgroundControllerInput.set(enable, is_game_specific);
}
bool getImeAccessibilityEnabled() {
return imeAccessibilityEnabled.get();
}
void setImeAccessibilityEnabled(bool enable, bool is_game_specific) {
imeAccessibilityEnabled.set(enable, is_game_specific);
}
bool getImeUrlMailShortPanel() {
return imeUrlMailShortPanel.get();
}
void setImeUrlMailShortPanel(bool enable, bool is_game_specific) {
imeUrlMailShortPanel.set(enable, is_game_specific);
}
bool getFsrEnabled() {
return fsrEnabled.get();
}
@ -923,6 +941,8 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
isMotionControlsEnabled.setFromToml(input, "isMotionControlsEnabled", is_game_specific);
useUnifiedInputConfig.setFromToml(input, "useUnifiedInputConfig", is_game_specific);
backgroundControllerInput.setFromToml(input, "backgroundControllerInput", is_game_specific);
imeAccessibilityEnabled.setFromToml(input, "imeAccessibilityEnabled", is_game_specific);
imeUrlMailShortPanel.setFromToml(input, "imeUrlMailShortPanel", is_game_specific);
usbDeviceBackend.setFromToml(input, "usbDeviceBackend", is_game_specific);
}
@ -1109,6 +1129,9 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
is_game_specific);
backgroundControllerInput.setTomlValue(data, "Input", "backgroundControllerInput",
is_game_specific);
imeAccessibilityEnabled.setTomlValue(data, "Input", "imeAccessibilityEnabled",
is_game_specific);
imeUrlMailShortPanel.setTomlValue(data, "Input", "imeUrlMailShortPanel", is_game_specific);
usbDeviceBackend.setTomlValue(data, "Input", "usbDeviceBackend", is_game_specific);
micDevice.setTomlValue(data, "Audio", "micDevice", is_game_specific);

View File

@ -148,6 +148,10 @@ std::string getDefaultControllerID();
void setDefaultControllerID(std::string id);
bool getBackgroundControllerInput();
void setBackgroundControllerInput(bool enable, bool is_game_specific = false);
bool getImeAccessibilityEnabled();
void setImeAccessibilityEnabled(bool enable, bool is_game_specific = false);
bool getImeUrlMailShortPanel();
void setImeUrlMailShortPanel(bool enable, bool is_game_specific = false);
bool getLoggingEnabled();
void setLoggingEnabled(bool enable, bool is_game_specific = false);
bool getFsrEnabled();

View File

@ -4,6 +4,7 @@
#include <queue>
#include "common/logging/log.h"
#include "core/libraries/ime/ime.h"
#include "core/libraries/ime/ime_dialog.h"
#include "core/libraries/ime/ime_error.h"
#include "core/libraries/ime/ime_ui.h"
#include "core/libraries/libs.h"
@ -202,7 +203,8 @@ int PS4_SYSV_ABI sceImeConfigSet() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceImeConfirmCandidate() {
int PS4_SYSV_ABI sceImeConfirmCandidate(s32 index) {
(void)index;
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}
@ -252,7 +254,10 @@ int PS4_SYSV_ABI sceImeForTestFunction() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceImeGetPanelPositionAndForm() {
int PS4_SYSV_ABI sceImeGetPanelPositionAndForm(OrbisImePositionAndForm* posForm) {
if (!posForm) {
return ORBIS_OK;
}
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}
@ -274,45 +279,26 @@ Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u3
return Error::INVALID_ADDRESS;
}
if (static_cast<u32>(param->option) & ~0x7BFF) { // Basic check for invalid options
LOG_ERROR(Lib_Ime, "Invalid option: {:032b}", static_cast<u32>(param->option));
return Error::INVALID_OPTION;
}
switch (param->type) {
case OrbisImeType::Default:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: Default ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::BasicLatin:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: BasicLatin ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Url:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: Url ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Mail:
// We set our custom sizes, commented sizes are the original ones
*width = 500; // 793
*height = 100; // 408
LOG_DEBUG(Lib_Ime, "param->type: Mail ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Number:
*width = 370;
*height = 402;
LOG_DEBUG(Lib_Ime, "param->type: Number ({})", static_cast<u32>(param->type));
break;
default:
LOG_ERROR(Lib_Ime, "Invalid param->type: ({})", static_cast<u32>(param->type));
return Error::INVALID_TYPE;
}
OrbisImeDialogParam dialog_param{};
dialog_param.user_id = param->user_id;
dialog_param.type = param->type;
dialog_param.supported_languages = param->supported_languages;
dialog_param.enter_label = param->enter_label;
dialog_param.input_method = param->input_method;
dialog_param.filter = param->filter;
dialog_param.option = param->option;
dialog_param.max_text_length = param->maxTextLength;
dialog_param.input_text_buffer = param->inputTextBuffer;
dialog_param.posx = param->posx;
dialog_param.posy = param->posy;
dialog_param.horizontal_alignment = param->horizontal_alignment;
dialog_param.vertical_alignment = param->vertical_alignment;
dialog_param.placeholder = nullptr;
dialog_param.title = nullptr;
const Error ret = Libraries::ImeDialog::sceImeDialogGetPanelSize(&dialog_param, width, height);
LOG_DEBUG(Lib_Ime, "IME panel size: width={}, height={}", *width, *height);
return Error::OK;
return ret;
}
Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceUserId userId) {
@ -339,7 +325,11 @@ Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceU
return Error::OK;
}
int PS4_SYSV_ABI sceImeKeyboardGetInfo() {
int PS4_SYSV_ABI sceImeKeyboardGetInfo(u32 resourceId, OrbisImeKeyboardInfo* info) {
(void)resourceId;
if (info) {
*info = {};
}
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}
@ -463,7 +453,10 @@ int PS4_SYSV_ABI sceImeKeyboardOpenInternal() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceImeKeyboardSetMode() {
int PS4_SYSV_ABI sceImeKeyboardSetMode(Libraries::UserService::OrbisUserServiceUserId userId,
u32 mode) {
(void)userId;
(void)mode;
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}
@ -681,7 +674,8 @@ void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param) {
param->user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID;
}
int PS4_SYSV_ABI sceImeSetCandidateIndex() {
int PS4_SYSV_ABI sceImeSetCandidateIndex(s32 index) {
(void)index;
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}
@ -714,7 +708,10 @@ Error PS4_SYSV_ABI sceImeSetText(const char16_t* text, u32 length) {
return g_ime_handler->SetText(text, length);
}
int PS4_SYSV_ABI sceImeSetTextGeometry() {
int PS4_SYSV_ABI sceImeSetTextGeometry(OrbisImeTextAreaMode mode,
const OrbisImeTextGeometry* geometry) {
(void)mode;
(void)geometry;
LOG_ERROR(Lib_Ime, "(STUBBED) called");
return ORBIS_OK;
}

View File

@ -21,7 +21,7 @@ int PS4_SYSV_ABI sceImeCheckUpdateTextInfo();
Error PS4_SYSV_ABI sceImeClose();
int PS4_SYSV_ABI sceImeConfigGet();
int PS4_SYSV_ABI sceImeConfigSet();
int PS4_SYSV_ABI sceImeConfirmCandidate();
int PS4_SYSV_ABI sceImeConfirmCandidate(s32 index);
int PS4_SYSV_ABI sceImeDicAddWord();
int PS4_SYSV_ABI sceImeDicDeleteLearnDics();
int PS4_SYSV_ABI sceImeDicDeleteUserDics();
@ -31,25 +31,27 @@ int PS4_SYSV_ABI sceImeDicReplaceWord();
int PS4_SYSV_ABI sceImeDisableController();
int PS4_SYSV_ABI sceImeFilterText();
int PS4_SYSV_ABI sceImeForTestFunction();
int PS4_SYSV_ABI sceImeGetPanelPositionAndForm();
int PS4_SYSV_ABI sceImeGetPanelPositionAndForm(OrbisImePositionAndForm* posForm);
Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* height);
Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceUserId userId);
int PS4_SYSV_ABI sceImeKeyboardGetInfo();
int PS4_SYSV_ABI sceImeKeyboardGetInfo(u32 resourceId, OrbisImeKeyboardInfo* info);
Error PS4_SYSV_ABI
sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userId,
OrbisImeKeyboardResourceIdArray* resourceIdArray);
Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUserId userId,
const OrbisImeKeyboardParam* param);
int PS4_SYSV_ABI sceImeKeyboardOpenInternal();
int PS4_SYSV_ABI sceImeKeyboardSetMode();
int PS4_SYSV_ABI sceImeKeyboardSetMode(Libraries::UserService::OrbisUserServiceUserId userId,
u32 mode);
int PS4_SYSV_ABI sceImeKeyboardUpdate();
Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExtended* extended);
int PS4_SYSV_ABI sceImeOpenInternal();
void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param);
int PS4_SYSV_ABI sceImeSetCandidateIndex();
int PS4_SYSV_ABI sceImeSetCandidateIndex(s32 index);
Error PS4_SYSV_ABI sceImeSetCaret(const OrbisImeCaret* caret);
Error PS4_SYSV_ABI sceImeSetText(const char16_t* text, u32 length);
int PS4_SYSV_ABI sceImeSetTextGeometry();
int PS4_SYSV_ABI sceImeSetTextGeometry(OrbisImeTextAreaMode mode,
const OrbisImeTextGeometry* geometry);
Error PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler);
int PS4_SYSV_ABI sceImeVshClearPreedit();
int PS4_SYSV_ABI sceImeVshClose();

View File

@ -375,6 +375,16 @@ enum class OrbisImeKeyboardType : u32 {
HUNGARIAN = 37,
};
enum class OrbisImeKeyboardDeviceType : u32 {
Keyboard = 0,
Osk = 1,
};
enum class OrbisImeKeyboardStatus : u32 {
Disconnected = 0,
Connected = 1,
};
enum class OrbisImeDeviceType : u32 {
None = 0,
Controller = 1,
@ -438,6 +448,16 @@ struct OrbisImeKeyboardResourceIdArray {
u32 resource_id[5];
};
struct OrbisImeKeyboardInfo {
Libraries::UserService::OrbisUserServiceUserId user_id;
OrbisImeKeyboardDeviceType device;
OrbisImeKeyboardType type;
u32 repeat_delay;
u32 repeat_rate;
OrbisImeKeyboardStatus status;
s8 reserved[12];
};
enum class OrbisImeCaretMovementDirection : u32 {
Still = 0,
Left = 1,
@ -462,6 +482,23 @@ enum class OrbisImePanelType : u32 {
Accessibility = 6,
};
struct OrbisImePositionAndForm {
OrbisImePanelType type;
f32 posx;
f32 posy;
OrbisImeHorizontalAlignment horizontal_alignment;
OrbisImeVerticalAlignment vertical_alignment;
u32 width;
u32 height;
};
struct OrbisImeTextGeometry {
f32 x;
f32 y;
u32 width;
u32 height;
};
union OrbisImeEventParam {
OrbisImeRect rect;
OrbisImeEditText text;

File diff suppressed because it is too large Load Diff

View File

@ -27,14 +27,14 @@ enum class OrbisImeDialogEndStatus : u32 {
struct OrbisImeDialogResult {
OrbisImeDialogEndStatus endstatus;
s32 reserved[12];
s8 reserved[12];
};
Error PS4_SYSV_ABI sceImeDialogAbort();
Error PS4_SYSV_ABI sceImeDialogForceClose();
Error PS4_SYSV_ABI sceImeDialogForTestFunction();
int PS4_SYSV_ABI sceImeDialogGetCurrentStarState();
int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm();
int PS4_SYSV_ABI sceImeDialogGetCurrentStarState(s64 param_1);
int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm(OrbisImePositionAndForm* posForm);
Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u32* width,
u32* height);
Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param,
@ -43,10 +43,12 @@ Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* p
Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result);
OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus();
Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended);
int PS4_SYSV_ABI sceImeDialogInitInternal();
int PS4_SYSV_ABI sceImeDialogInitInternal2();
int PS4_SYSV_ABI sceImeDialogInitInternal3();
int PS4_SYSV_ABI sceImeDialogSetPanelPosition();
int PS4_SYSV_ABI sceImeDialogInitInternal(OrbisImeDialogParam* param,
OrbisImeParamExtended* extended);
int PS4_SYSV_ABI sceImeDialogInitInternal2(int* param_1, u32* param_2, u32 param_3, u64 param_4);
int PS4_SYSV_ABI sceImeDialogInitInternal3(int* param_1, u32* param_2, u32 param_3, u64 param_4,
u32 param_5, u32 param_6);
int PS4_SYSV_ABI sceImeDialogSetPanelPosition(s32 posx, s32 posy);
Error PS4_SYSV_ABI sceImeDialogTerm();
void RegisterLib(Core::Loader::SymbolsResolver* sym);

View File

@ -1,28 +1,119 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cstring>
#include <cwchar>
#include <string>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include <magic_enum/magic_enum.hpp>
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/debug_state.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/ime/ime_dialog.h"
#include "core/libraries/ime/ime_dialog_ui.h"
#include "core/libraries/ime/ime_kb_layout.h"
#include "core/memory.h"
#include "core/tls.h"
#include "imgui/imgui_std.h"
using namespace ImGui;
static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f};
namespace Libraries::ImeDialog {
namespace {
bool IsMappedGuestBuffer(const void* ptr, size_t bytes) {
if (!ptr || bytes == 0) {
return false;
}
auto* memory = Core::Memory::Instance();
if (!memory) {
return false;
}
return memory->IsValidMapping(reinterpret_cast<VAddr>(ptr), bytes);
}
size_t BoundedUtf16Length(const char16_t* text, size_t max_len) {
if (!text || max_len == 0) {
return 0;
}
for (size_t i = 0; i < max_len; ++i) {
if (text[i] == u'\0') {
return i;
}
}
return max_len;
}
int Utf8CharCount(const char* text, int byte_len) {
if (!text || byte_len <= 0) {
return 0;
}
return ImTextCountCharsFromUtf8(text, text + byte_len);
}
int Utf8ByteIndexFromCharIndex(const char* text, int char_index) {
if (!text || char_index <= 0) {
return 0;
}
const char* p = text;
int count = 0;
while (*p && count < char_index) {
unsigned int c = 0;
const int step = ImTextCharFromUtf8(&c, p, nullptr);
if (step <= 0) {
break;
}
p += step;
++count;
}
return static_cast<int>(p - text);
}
int Utf16CountFromUtf8Range(const char* text, const char* end) {
if (!text) {
return 0;
}
const char* range_end = end ? end : (text + std::strlen(text));
std::array<char16_t, ORBIS_IME_DIALOG_MAX_TEXT_LENGTH + 1> tmp{};
ImTextStrFromUtf8(reinterpret_cast<ImWchar*>(tmp.data()), static_cast<int>(tmp.size()), text,
range_end);
return static_cast<int>(BoundedUtf16Length(tmp.data(), tmp.size() - 1));
}
int Utf8ByteIndexFromUtf16Index(const char* text, int utf16_index) {
if (!text || utf16_index <= 0) {
return 0;
}
const char* p = text;
int count = 0;
while (*p && count < utf16_index) {
unsigned int c = 0;
const int step = ImTextCharFromUtf8(&c, p, nullptr);
if (step <= 0) {
break;
}
const int utf16_units = (c > 0xFFFF) ? 2 : 1;
if (count + utf16_units > utf16_index) {
break;
}
count += utf16_units;
p += step;
}
return static_cast<int>(p - text);
}
} // namespace
ImeDialogState::ImeDialogState()
: input_changed(false), user_id(-1), is_multi_line(false), is_numeric(false),
type(OrbisImeType::Default), enter_label(OrbisImeEnterLabel::Default), text_filter(nullptr),
keyboard_filter(nullptr), max_text_length(ORBIS_IME_DIALOG_MAX_TEXT_LENGTH),
text_buffer(nullptr), title(), placeholder(), current_text() {}
text_buffer(nullptr), original_text(), title(), placeholder(), current_text() {}
ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param,
const OrbisImeParamExtended* extended) {
@ -35,6 +126,7 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param,
user_id = param->user_id;
is_multi_line = True(param->option & OrbisImeOption::MULTILINE);
use_over2k = True(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES);
is_numeric = param->type == OrbisImeType::Number;
type = param->type;
enter_label = param->enter_label;
@ -42,6 +134,21 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param,
keyboard_filter = extended ? extended->ext_keyboard_filter : nullptr;
max_text_length = param->max_text_length;
text_buffer = param->input_text_buffer;
LOG_INFO(Lib_ImeDialog,
"ImeDialogState: option=0x{:X} (multiline={}, password={}, ext_kbd={}, fixed_pos={}, "
"over2k={}), enter_label={}, type={}",
static_cast<u32>(param->option), is_multi_line,
True(param->option & OrbisImeOption::PASSWORD),
True(param->option & OrbisImeOption::EXT_KEYBOARD),
True(param->option & OrbisImeOption::FIXED_POSITION),
True(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES),
static_cast<u32>(enter_label), static_cast<u32>(type));
LOG_DEBUG(Lib_ImeDialog,
"ImeDialogState: user_id={}, type={}, enter_label={}, multiline={}, numeric={}, "
"max_len={}, text_filter={}, keyboard_filter={}",
static_cast<u32>(user_id), static_cast<u32>(type), static_cast<u32>(enter_label),
is_multi_line, is_numeric, max_text_length, (void*)text_filter,
(void*)keyboard_filter);
if (param->title) {
std::size_t title_len = std::char_traits<char16_t>::length(param->title);
@ -64,20 +171,56 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param,
}
}
std::size_t text_len = std::char_traits<char16_t>::length(text_buffer);
if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(),
ORBIS_IME_DIALOG_MAX_TEXT_LENGTH * 4 + 1)) {
LOG_ERROR(Lib_ImeDialog, "Failed to convert text to utf8 encoding");
std::size_t text_len = 0;
if (text_buffer) {
const size_t bytes = (static_cast<size_t>(max_text_length) + 1) * sizeof(char16_t);
if (IsMappedGuestBuffer(text_buffer, bytes)) {
text_len = BoundedUtf16Length(text_buffer, max_text_length);
} else {
LOG_ERROR(Lib_ImeDialog, "ImeDialogState: input_text_buffer not mapped");
}
}
if (text_len > max_text_length) {
text_len = max_text_length;
}
original_text.resize(static_cast<std::size_t>(max_text_length) + 1, u'\0');
if (text_buffer) {
for (std::size_t i = 0; i < text_len; ++i) {
original_text[i] = text_buffer[i];
}
}
if (text_buffer) {
if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(),
ORBIS_IME_DIALOG_MAX_TEXT_LENGTH * 4 + 1)) {
LOG_ERROR(Lib_ImeDialog, "Failed to convert text to utf8 encoding");
}
}
caret_index = Utf16CountFromUtf8Range(
current_text.begin(), current_text.begin() + static_cast<int>(current_text.size()));
caret_byte_index = static_cast<int>(current_text.size());
caret_dirty = true;
panel_layout_valid = (sceImeDialogGetPanelPositionAndForm(&panel_layout) == ORBIS_OK);
if (extended) {
(void)sceImeDialogGetPanelSizeExtended(param, extended, &panel_req_width,
&panel_req_height);
} else {
(void)sceImeDialogGetPanelSize(param, &panel_req_width, &panel_req_height);
}
LOG_DEBUG(Lib_ImeDialog, "ImeDialogState: initial_text_len={}", current_text.size());
}
ImeDialogState::ImeDialogState(ImeDialogState&& other) noexcept
: input_changed(other.input_changed), user_id(other.user_id),
: input_changed(other.input_changed), caret_index(other.caret_index),
caret_byte_index(other.caret_byte_index), caret_dirty(other.caret_dirty),
use_over2k(other.use_over2k), panel_layout(other.panel_layout),
panel_layout_valid(other.panel_layout_valid), panel_req_width(other.panel_req_width),
panel_req_height(other.panel_req_height), user_id(other.user_id),
is_multi_line(other.is_multi_line), is_numeric(other.is_numeric), type(other.type),
enter_label(other.enter_label), text_filter(other.text_filter),
keyboard_filter(other.keyboard_filter), max_text_length(other.max_text_length),
text_buffer(other.text_buffer), title(std::move(other.title)),
placeholder(std::move(other.placeholder)), current_text(other.current_text) {
text_buffer(other.text_buffer), original_text(std::move(other.original_text)),
title(std::move(other.title)), placeholder(std::move(other.placeholder)),
current_text(other.current_text) {
other.text_buffer = nullptr;
}
@ -85,6 +228,14 @@ ImeDialogState::ImeDialogState(ImeDialogState&& other) noexcept
ImeDialogState& ImeDialogState::operator=(ImeDialogState&& other) {
if (this != &other) {
input_changed = other.input_changed;
caret_index = other.caret_index;
caret_byte_index = other.caret_byte_index;
caret_dirty = other.caret_dirty;
use_over2k = other.use_over2k;
panel_layout = other.panel_layout;
panel_layout_valid = other.panel_layout_valid;
panel_req_width = other.panel_req_width;
panel_req_height = other.panel_req_height;
user_id = other.user_id;
is_multi_line = other.is_multi_line;
is_numeric = other.is_numeric;
@ -94,6 +245,7 @@ ImeDialogState& ImeDialogState::operator=(ImeDialogState&& other) {
keyboard_filter = other.keyboard_filter;
max_text_length = other.max_text_length;
text_buffer = other.text_buffer;
original_text = std::move(other.original_text);
title = std::move(other.title);
placeholder = std::move(other.placeholder);
current_text = other.current_text;
@ -104,13 +256,76 @@ ImeDialogState& ImeDialogState::operator=(ImeDialogState&& other) {
return *this;
}
bool ImeDialogState::CopyTextToOrbisBuffer() {
bool ImeDialogState::CopyTextToOrbisBuffer(bool use_original) {
if (!text_buffer) {
LOG_DEBUG(Lib_ImeDialog, "CopyTextToOrbisBuffer: no text_buffer");
return false;
}
return ConvertUTF8ToOrbis(current_text.begin(), current_text.capacity(), text_buffer,
static_cast<std::size_t>(max_text_length) + 1);
if (use_original) {
const std::size_t count =
original_text.empty() ? 0 : static_cast<std::size_t>(max_text_length) + 1;
if (count > 0) {
std::copy(original_text.begin(), original_text.end(), text_buffer);
}
LOG_DEBUG(Lib_ImeDialog, "CopyTextToOrbisBuffer: restored original");
return true;
}
const std::size_t utf8_len = current_text.size();
const bool ok = ConvertUTF8ToOrbis(current_text.begin(), utf8_len, text_buffer,
static_cast<std::size_t>(max_text_length) + 1);
LOG_DEBUG(Lib_ImeDialog, "CopyTextToOrbisBuffer: {}", ok ? "ok" : "failed");
return ok;
}
bool ImeDialogState::NormalizeNewlines() {
if (current_text.size() == 0) {
return false;
}
std::string src = current_text.to_string();
std::string out;
out.reserve(src.size());
bool changed = false;
for (size_t i = 0; i < src.size(); ++i) {
const char ch = src[i];
if (ch == '\r') {
if (i + 1 < src.size() && src[i + 1] == '\n') {
++i;
}
out.push_back('\n');
changed = true;
} else {
out.push_back(ch);
}
}
if (changed) {
current_text.FromString(out);
}
return changed;
}
bool ImeDialogState::ClampCurrentTextToMaxLen() {
if (current_text.size() == 0 || max_text_length == 0) {
return false;
}
const int utf16_len = Utf16CountFromUtf8Range(
current_text.begin(), current_text.begin() + static_cast<int>(current_text.size()));
if (utf16_len <= static_cast<int>(max_text_length)) {
return false;
}
std::vector<char16_t> utf16(static_cast<size_t>(max_text_length) + 1, u'\0');
ImTextStrFromUtf8(reinterpret_cast<ImWchar*>(utf16.data()),
static_cast<int>(max_text_length) + 1, current_text.begin(),
current_text.begin() + current_text.size());
size_t len = BoundedUtf16Length(utf16.data(), static_cast<size_t>(max_text_length));
std::string out;
out.resize(len * 4 + 1, '\0');
ImTextStrToUtf8(out.data(), out.size(), reinterpret_cast<const ImWchar*>(utf16.data()),
reinterpret_cast<const ImWchar*>(utf16.data()) + len);
out.resize(std::strlen(out.c_str()));
current_text.FromString(out);
return true;
}
bool ImeDialogState::CallTextFilter() {
@ -121,15 +336,17 @@ bool ImeDialogState::CallTextFilter() {
input_changed = false;
char16_t src_text[ORBIS_IME_DIALOG_MAX_TEXT_LENGTH + 1] = {0};
u32 src_text_length = current_text.size();
u32 src_text_length = 0;
char16_t out_text[ORBIS_IME_DIALOG_MAX_TEXT_LENGTH + 1] = {0};
u32 out_text_length = ORBIS_IME_DIALOG_MAX_TEXT_LENGTH;
if (!ConvertUTF8ToOrbis(current_text.begin(), src_text_length, src_text,
ORBIS_IME_DIALOG_MAX_TEXT_LENGTH)) {
if (!ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), src_text,
ORBIS_IME_DIALOG_MAX_TEXT_LENGTH + 1)) {
LOG_ERROR(Lib_ImeDialog, "Failed to convert text to orbis encoding");
return false;
}
src_text_length = static_cast<u32>(
BoundedUtf16Length(src_text, static_cast<size_t>(ORBIS_IME_DIALOG_MAX_TEXT_LENGTH)));
int ret = text_filter(out_text, &out_text_length, src_text, src_text_length);
@ -143,6 +360,17 @@ bool ImeDialogState::CallTextFilter() {
return false;
}
const bool changed = NormalizeNewlines() | ClampCurrentTextToMaxLen();
const int new_len = Utf16CountFromUtf8Range(
current_text.begin(), current_text.begin() + static_cast<int>(current_text.size()));
if (caret_index > new_len) {
caret_index = new_len;
caret_dirty = true;
} else if (changed) {
caret_dirty = true;
}
CopyTextToOrbisBuffer(false);
return true;
}
@ -168,7 +396,8 @@ bool ImeDialogState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t
bool ImeDialogState::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<ImWchar*>(orbis_text), orbis_text_len, utf8_text, nullptr);
const char* utf8_end = utf8_text ? (utf8_text + utf8_text_len) : nullptr;
ImTextStrFromUtf8(reinterpret_cast<ImWchar*>(orbis_text), orbis_text_len, utf8_text, utf8_end);
return true;
}
@ -225,6 +454,18 @@ void ImeDialogUi::Free() {
RemoveLayer(this);
}
void ImeDialogUi::FinishDialog(OrbisImeDialogEndStatus endstatus, bool restore_original,
const char* reason) {
if (!status || !result || !state) {
return;
}
state->CopyTextToOrbisBuffer(restore_original);
*status = OrbisImeDialogStatus::Finished;
result->endstatus = endstatus;
LOG_INFO(Lib_ImeDialog, "ImeDialog {} -> status=Finished", reason ? reason : "Done");
Free();
}
void ImeDialogUi::Draw() {
std::unique_lock<std::mutex> lock{draw_mutex};
@ -233,21 +474,67 @@ void ImeDialogUi::Draw() {
}
if (!status || *status != OrbisImeDialogStatus::Running) {
Free();
return;
}
const auto& ctx = *GetCurrentContext();
const auto& io = ctx.IO;
ImVec2 window_size;
constexpr int key_cols = 10;
constexpr int key_rows = 6;
OrbisImePositionAndForm layout = state->panel_layout;
const bool has_layout = state->panel_layout_valid;
const auto viewport = Libraries::Ime::ComputeImeViewportMetrics(state->use_over2k);
const ImVec2 viewport_size = viewport.size;
const ImVec2 viewport_offset = viewport.offset;
const float scale_x = viewport.scale_x;
const float scale_y = viewport.scale_y;
const float ui_scale = viewport.ui_scale;
if (state->is_multi_line) {
window_size = {500.0f, 300.0f};
ImVec2 window_size;
const bool has_panel_size = (has_layout && layout.width > 0 && layout.height > 0) ||
(state->panel_req_width > 0 && state->panel_req_height > 0);
if (has_layout && layout.width > 0 && layout.height > 0) {
window_size = {layout.width * scale_x, layout.height * scale_y};
} else if (state->panel_req_width > 0 && state->panel_req_height > 0) {
window_size = {static_cast<float>(state->panel_req_width) * scale_x,
static_cast<float>(state->panel_req_height) * scale_y};
} else {
window_size = {500.0f, 150.0f};
window_size = {std::min(std::max(0.0f, viewport_size.x - 40.0f), 640.0f),
std::min(std::max(0.0f, viewport_size.y - 40.0f), 420.0f)};
}
if (!has_panel_size) {
window_size.x = std::max(window_size.x, 320.0f);
window_size.y = std::max(window_size.y, 240.0f);
}
CentralizeNextWindow();
const float panel_w = window_size.x;
const float panel_h = window_size.y;
if (has_layout) {
float x = viewport_offset.x + layout.posx * scale_x;
float y = viewport_offset.y + layout.posy * scale_y;
if (layout.horizontal_alignment == OrbisImeHorizontalAlignment::Center) {
x -= window_size.x * 0.5f;
} else if (layout.horizontal_alignment == OrbisImeHorizontalAlignment::Right) {
x -= window_size.x;
}
if (layout.vertical_alignment == OrbisImeVerticalAlignment::Center) {
y -= window_size.y * 0.5f;
} else if (layout.vertical_alignment == OrbisImeVerticalAlignment::Bottom) {
y -= window_size.y;
}
x = std::clamp(x, viewport_offset.x,
viewport_offset.x + std::max(0.0f, viewport_size.x - window_size.x));
y = std::clamp(y, viewport_offset.y,
viewport_offset.y + std::max(0.0f, viewport_size.y - window_size.y));
SetNextWindowPos({x, y});
} else {
SetNextWindowPos({viewport_offset.x + viewport_size.x * 0.5f,
viewport_offset.y + viewport_size.y * 0.5f},
ImGuiCond_Always, {0.5f, 0.5f});
}
SetNextWindowSize(window_size);
SetNextWindowCollapsed(false);
@ -258,80 +545,153 @@ void ImeDialogUi::Draw() {
if (Begin("IME Dialog##ImeDialog", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) {
DrawPrettyBackground();
const Libraries::Ime::ImePanelMetricsConfig metrics_cfg{
.panel_w = panel_w,
.panel_h = panel_h,
.multiline = state->is_multi_line,
.show_title = !state->title.empty(),
.base_font_size = GetFontSize(),
.window_pos = GetWindowPos(),
};
const Libraries::Ime::ImePanelMetrics metrics =
Libraries::Ime::ComputeImePanelMetrics(metrics_cfg);
SetWindowFontScale(std::max(ui_scale, metrics.input_font_scale));
if (first_render) {
const auto game_res = DebugState.game_resolution;
const auto out_res = DebugState.output_resolution;
const float req_w = has_layout ? static_cast<float>(layout.width)
: static_cast<float>(state->panel_req_width);
const float req_h = has_layout ? static_cast<float>(layout.height)
: static_cast<float>(state->panel_req_height);
LOG_INFO(Lib_ImeDialog,
"ImeDialog UI metrics: game_res={}x{}, out_res={}x{}, viewport_pos=({}, {}), "
"viewport_size=({}, {}), base={}x{}, scale=({:.4f}, {:.4f}), "
"panel_req=({}, {}), panel_scaled=({}, {})",
game_res.first, game_res.second, out_res.first, out_res.second,
viewport_offset.x, viewport_offset.y, viewport_size.x, viewport_size.y,
viewport.base_w, viewport.base_h, scale_x, scale_y, req_w, req_h,
window_size.x, window_size.y);
}
if (!state->title.empty()) {
SetWindowFontScale(1.7f);
SetCursorPosY(0.0f);
SetCursorPosX(metrics.padding_x);
SetWindowFontScale(metrics.label_font_scale);
TextUnformatted(state->title.data());
SetWindowFontScale(1.0f);
SetWindowFontScale(std::max(ui_scale, metrics.input_font_scale));
}
if (state->is_multi_line) {
DrawMultiLineInputText();
DrawMultiLineInputText(metrics);
} else {
DrawInputText();
DrawInputText(metrics);
}
SetCursorPosY(GetCursorPosY() + 10.0f);
auto* draw = GetWindowDrawList();
const ImU32 pane_bg = IM_COL32(18, 18, 18, 255);
const ImU32 pane_border = IM_COL32(70, 70, 70, 255);
draw->AddRectFilled(metrics.predict_pos,
{metrics.predict_pos.x + metrics.predict_size.x,
metrics.predict_pos.y + metrics.predict_size.y},
pane_bg, metrics.corner_radius);
draw->AddRect(metrics.predict_pos,
{metrics.predict_pos.x + metrics.predict_size.x,
metrics.predict_pos.y + metrics.predict_size.y},
pane_border, metrics.corner_radius);
PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 1.0f));
PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.35f, 0.35f, 1.0f));
SetCursorScreenPos(metrics.close_pos);
const bool cancel_pressed =
Button("X##ImeDialogClose", {metrics.close_size.x, metrics.close_size.y});
PopStyleColor(3);
const char* button_text;
SetCursorScreenPos(metrics.kb_pos);
switch (state->enter_label) {
case OrbisImeEnterLabel::Go:
button_text = "Go##ImeDialogOK";
break;
case OrbisImeEnterLabel::Search:
button_text = "Search##ImeDialogOK";
break;
case OrbisImeEnterLabel::Send:
button_text = "Send##ImeDialogOK";
break;
case OrbisImeEnterLabel::Default:
default:
button_text = "OK##ImeDialogOK";
break;
if (!accept_armed) {
if (!IsKeyDown(ImGuiKey_Enter) && !IsKeyDown(ImGuiKey_GamepadFaceDown)) {
accept_armed = true;
LOG_DEBUG(Lib_ImeDialog, "ImeDialog: accept armed");
}
}
const bool allow_key_accept = accept_armed;
bool accept_pressed =
(allow_key_accept && !state->is_multi_line && IsKeyPressed(ImGuiKey_Enter)) ||
(allow_key_accept && IsKeyPressed(ImGuiKey_GamepadFaceDown));
Libraries::Ime::ImeKbGridLayout kb_layout{};
kb_layout.pos = metrics.kb_pos;
kb_layout.size = metrics.kb_size;
kb_layout.key_gap_x = metrics.key_gap;
kb_layout.key_gap_y = metrics.key_gap;
kb_layout.key_h = metrics.key_h;
kb_layout.cols = key_cols;
kb_layout.rows = key_rows;
kb_layout.corner_radius = metrics.corner_radius;
Libraries::Ime::ImeKbDrawParams kb_params{};
kb_params.enter_label = state->enter_label;
kb_params.key_bg_alt = IM_COL32(45, 45, 45, 255);
Libraries::Ime::ImeKbDrawState kb_state{};
SetWindowFontScale(metrics.key_font_scale);
Libraries::Ime::DrawImeKeyboardGrid(kb_layout, kb_params, kb_state);
SetWindowFontScale(metrics.input_font_scale);
if (kb_state.done_pressed) {
accept_pressed = true;
}
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;
Dummy({metrics.kb_size.x, metrics.kb_size.y + metrics.padding_bottom});
SetCursorPosX(button_start_pos);
if (Button(button_text, BUTTON_SIZE) ||
(!state->is_multi_line && IsKeyPressed(ImGuiKey_Enter))) {
*status = OrbisImeDialogStatus::Finished;
result->endstatus = OrbisImeDialogEndStatus::Ok;
}
SameLine(0.0f, button_spacing);
if (Button("Cancel##ImeDialogCancel", BUTTON_SIZE)) {
*status = OrbisImeDialogStatus::Finished;
result->endstatus = OrbisImeDialogEndStatus::UserCanceled;
if (accept_pressed) {
LOG_INFO(Lib_ImeDialog, "ImeDialog OK text(len={}): \"{}\"", state->current_text.size(),
state->current_text.begin());
FinishDialog(OrbisImeDialogEndStatus::Ok, false, "OK");
} else if (cancel_pressed) {
FinishDialog(OrbisImeDialogEndStatus::UserCanceled, true, "Cancel");
}
SetWindowFontScale(1.0f);
}
End();
first_render = false;
}
void ImeDialogUi::DrawInputText() {
ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f};
SetCursorPosX(20.0f);
void ImeDialogUi::DrawInputText(const Libraries::Ime::ImePanelMetrics& metrics) {
const ImVec2 input_size = metrics.input_size;
SetCursorPos(metrics.input_pos_local);
if (first_render) {
SetKeyboardFocusHere();
}
const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data();
if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(),
state->max_text_length * 4 + 1, input_size,
ImGuiInputTextFlags_CallbackCharFilter, InputTextCallback, this)) {
ImGuiInputTextFlags_CallbackCharFilter | ImGuiInputTextFlags_CallbackAlways,
InputTextCallback, this)) {
state->input_changed = true;
const bool changed = state->NormalizeNewlines() | state->ClampCurrentTextToMaxLen();
if (changed) {
const int buf_len = static_cast<int>(state->current_text.size());
const int caret_byte = std::clamp(state->caret_byte_index, 0, buf_len);
state->caret_index = Utf16CountFromUtf8Range(state->current_text.begin(),
state->current_text.begin() + caret_byte);
const int new_len = Utf16CountFromUtf8Range(
state->current_text.begin(),
state->current_text.begin() + static_cast<int>(state->current_text.size()));
if (state->caret_index > new_len) {
state->caret_index = new_len;
}
state->caret_dirty = true;
}
state->CopyTextToOrbisBuffer(false);
}
}
void ImeDialogUi::DrawMultiLineInputText() {
ImVec2 input_size = {GetWindowWidth() - 40.0f, 200.0f};
SetCursorPosX(20.0f);
void ImeDialogUi::DrawMultiLineInputText(const Libraries::Ime::ImePanelMetrics& metrics) {
const ImVec2 input_size = metrics.input_size;
SetCursorPos(metrics.input_pos_local);
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackCharFilter |
static_cast<ImGuiInputTextFlags>(ImGuiInputTextFlags_Multiline);
if (first_render) {
@ -339,8 +699,24 @@ void ImeDialogUi::DrawMultiLineInputText() {
}
const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data();
if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(),
state->max_text_length * 4 + 1, input_size, flags, InputTextCallback, this)) {
state->max_text_length * 4 + 1, input_size,
flags | ImGuiInputTextFlags_CallbackAlways, InputTextCallback, this)) {
state->input_changed = true;
const bool changed = state->ClampCurrentTextToMaxLen();
if (changed) {
const int buf_len = static_cast<int>(state->current_text.size());
const int caret_byte = std::clamp(state->caret_byte_index, 0, buf_len);
state->caret_index = Utf16CountFromUtf8Range(state->current_text.begin(),
state->current_text.begin() + caret_byte);
const int new_len = Utf16CountFromUtf8Range(
state->current_text.begin(),
state->current_text.begin() + static_cast<int>(state->current_text.size()));
if (state->caret_index > new_len) {
state->caret_index = new_len;
}
state->caret_dirty = true;
}
state->CopyTextToOrbisBuffer(false);
}
}
@ -348,6 +724,31 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) {
ImeDialogUi* ui = static_cast<ImeDialogUi*>(data->UserData);
ASSERT(ui);
if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) {
if (data->BufTextLen > 0) {
const int caret_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->CursorPos);
LOG_DEBUG(Lib_ImeDialog, "ImeDialog caret: buf_len={}, cursor_byte={}, caret_utf16={}",
data->BufTextLen, data->CursorPos, caret_utf16);
}
if (ui->state->caret_dirty) {
const int len_chars = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->BufTextLen);
int caret = ui->state->caret_index;
if (caret < 0) {
caret = 0;
} else if (caret > len_chars) {
caret = len_chars;
}
const int caret_byte = Utf8ByteIndexFromUtf16Index(data->Buf, caret);
data->CursorPos = caret_byte;
data->SelectionStart = caret_byte;
data->SelectionEnd = caret_byte;
ui->state->caret_dirty = false;
}
ui->state->caret_byte_index = data->CursorPos;
ui->state->caret_index = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->CursorPos);
return 0;
}
LOG_DEBUG(Lib_ImeDialog, ">> InputTextCallback: EventFlag={}, EventChar={}", data->EventFlag,
data->EventChar);
@ -359,10 +760,18 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) {
return 1;
}
if (ui->state->is_multi_line && (data->EventChar == '\n' || data->EventChar == '\r')) {
const int caret_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->CursorPos);
ui->state->caret_index = caret_utf16 + 1;
ui->state->caret_dirty = true;
}
if (!ui->state->keyboard_filter) {
LOG_DEBUG(Lib_ImeDialog, "InputTextCallback: no keyboard_filter, accepting char");
return 0;
}
LOG_DEBUG(Lib_ImeDialog, "InputTextCallback: skipping keyboard_filter on render thread");
return 0;
// ImGui encodes ImWchar32 as multi-byte UTF-8 characters
char* event_char = reinterpret_cast<char*>(&data->EventChar);
@ -398,6 +807,10 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) {
keep ? "true" : "false", out_keycode, out_status);
// TODO. set the keycode
if (!keep) {
LOG_INFO(Lib_ImeDialog, "InputTextCallback: keyboard_filter rejected char");
return 1;
}
return 0;
}

View File

@ -14,11 +14,26 @@
namespace Libraries::ImeDialog {
class ImeDialogUi;
} // namespace Libraries::ImeDialog
namespace Libraries::Ime {
struct ImePanelMetrics;
}
namespace Libraries::ImeDialog {
class ImeDialogState final {
friend ImeDialogUi;
bool input_changed = false;
int caret_index = 0;
int caret_byte_index = 0;
bool caret_dirty = false;
bool use_over2k = false;
OrbisImePositionAndForm panel_layout{};
bool panel_layout_valid = false;
u32 panel_req_width = 0;
u32 panel_req_height = 0;
s32 user_id{};
bool is_multi_line{};
@ -29,6 +44,7 @@ class ImeDialogState final {
OrbisImeExtKeyboardFilter keyboard_filter{};
u32 max_text_length{};
char16_t* text_buffer{};
std::vector<char16_t> original_text;
std::vector<char> title;
std::vector<char> placeholder;
@ -48,8 +64,10 @@ public:
ImeDialogState(ImeDialogState&& other) noexcept;
ImeDialogState& operator=(ImeDialogState&& other);
bool CopyTextToOrbisBuffer();
bool CopyTextToOrbisBuffer(bool use_original);
bool CallTextFilter();
bool NormalizeNewlines();
bool ClampCurrentTextToMaxLen();
private:
bool CallKeyboardFilter(const OrbisImeKeycode* src_keycode, u16* out_keycode, u32* out_status);
@ -66,6 +84,7 @@ class ImeDialogUi final : public ImGui::Layer {
OrbisImeDialogResult* result{};
bool first_render = true;
bool accept_armed = false;
std::mutex draw_mutex;
public:
@ -79,10 +98,11 @@ public:
void Draw() override;
private:
void FinishDialog(OrbisImeDialogEndStatus endstatus, bool restore_original, const char* reason);
void Free();
void DrawInputText();
void DrawMultiLineInputText();
void DrawInputText(const Libraries::Ime::ImePanelMetrics& metrics);
void DrawMultiLineInputText(const Libraries::Ime::ImePanelMetrics& metrics);
static int InputTextCallback(ImGuiInputTextCallbackData* data);
};

View File

@ -0,0 +1,237 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/libraries/ime/ime_kb_layout.h"
#include <algorithm>
#include <array>
#include "core/debug_state.h"
namespace Libraries::Ime {
namespace {
struct KeySpec {
int span;
const char* label;
bool is_done;
};
constexpr float kPanelBaseW = 793.0f;
constexpr float kPanelBaseHSingle = 528.0f;
constexpr float kPanelBaseHMulti = 628.0f;
constexpr float kLabelH = 57.0f;
constexpr float kInputHSingle = 50.0f;
constexpr float kInputHMulti = 151.0f;
constexpr float kPredictH = 53.0f;
constexpr float kPredictW = 740.0f;
constexpr float kCloseW = 53.0f;
constexpr float kKeysH = 316.0f;
constexpr float kKeyGap = 9.0f;
constexpr float kPadX = 26.0f;
constexpr float kPadBottomSingle = 26.0f;
constexpr float kPadBottomMulti = 26.0f;
constexpr float kKeyFontRatio = 0.035f;
constexpr float kCornerRatio = 0.004f;
constexpr float kSingleLineTextFill = 0.85f;
constexpr float kMultiLineTextFill = 0.85f;
constexpr int kMultiLineVisibleLines = 4;
constexpr int kKeyRows = 6;
constexpr int kKeyCols = 10;
const char* GetEnterLabel(OrbisImeEnterLabel label) {
switch (label) {
case OrbisImeEnterLabel::Go:
return "Go";
case OrbisImeEnterLabel::Search:
return "Search";
case OrbisImeEnterLabel::Send:
return "Send";
case OrbisImeEnterLabel::Default:
default:
return "Done";
}
}
} // namespace
ImeViewportMetrics ComputeImeViewportMetrics(bool use_over2k) {
ImeViewportMetrics metrics{};
metrics.base_w = use_over2k ? 3840.0f : 1920.0f;
metrics.base_h = use_over2k ? 2160.0f : 1080.0f;
const ImGuiIO& io = ImGui::GetIO();
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 base_pos = viewport ? viewport->WorkPos : ImVec2{0.0f, 0.0f};
ImVec2 base_size = viewport ? viewport->WorkSize : io.DisplaySize;
if (base_size.x <= 0.0f || base_size.y <= 0.0f) {
base_pos = {0.0f, 0.0f};
base_size = io.DisplaySize;
}
metrics.size = base_size;
metrics.offset = base_pos;
const auto out_res = DebugState.output_resolution;
if (out_res.first != 0 && out_res.second != 0) {
const float fb_scale_x =
io.DisplayFramebufferScale.x > 0.0f ? io.DisplayFramebufferScale.x : 1.0f;
const float fb_scale_y =
io.DisplayFramebufferScale.y > 0.0f ? io.DisplayFramebufferScale.y : 1.0f;
const float viewport_w = static_cast<float>(out_res.first) / fb_scale_x;
const float viewport_h = static_cast<float>(out_res.second) / fb_scale_y;
float offset_x = (base_size.x - viewport_w) * 0.5f;
float offset_y = (base_size.y - viewport_h) * 0.5f;
if (offset_x < 0.0f) {
offset_x = 0.0f;
}
if (offset_y < 0.0f) {
offset_y = 0.0f;
}
metrics.size = {viewport_w, viewport_h};
metrics.offset = {base_pos.x + offset_x, base_pos.y + offset_y};
}
metrics.scale_x = metrics.size.x / metrics.base_w;
metrics.scale_y = metrics.size.y / metrics.base_h;
metrics.ui_scale = std::min(metrics.scale_x, metrics.scale_y);
return metrics;
}
ImePanelMetrics ComputeImePanelMetrics(const ImePanelMetricsConfig& config) {
ImePanelMetrics metrics{};
metrics.panel_w = config.panel_w;
metrics.panel_h = config.panel_h;
metrics.padding_x = metrics.panel_w * (kPadX / kPanelBaseW);
metrics.padding_bottom =
metrics.panel_h * (config.multiline ? (kPadBottomMulti / kPanelBaseHMulti)
: (kPadBottomSingle / kPanelBaseHSingle));
const float panel_scale = metrics.panel_w / kPanelBaseW;
metrics.label_h = config.show_title ? (kLabelH * panel_scale) : metrics.padding_bottom;
metrics.input_h = metrics.panel_h * (config.multiline ? (kInputHMulti / kPanelBaseHMulti)
: (kInputHSingle / kPanelBaseHSingle));
metrics.predict_h = metrics.panel_h * (config.multiline ? (kPredictH / kPanelBaseHMulti)
: (kPredictH / kPanelBaseHSingle));
metrics.close_w = metrics.panel_w * (kCloseW / kPanelBaseW);
metrics.keys_h = metrics.panel_h * (config.multiline ? (kKeysH / kPanelBaseHMulti)
: (kKeysH / kPanelBaseHSingle));
metrics.key_gap = metrics.panel_w * (kKeyGap / kPanelBaseW);
metrics.corner_radius = metrics.panel_w * kCornerRatio;
const float base_font_size = config.base_font_size > 0.0f ? config.base_font_size : 1.0f;
metrics.label_font_scale = (metrics.label_h * 0.85f) / base_font_size;
metrics.input_font_scale =
(metrics.input_h * (config.multiline
? (kMultiLineTextFill / static_cast<float>(kMultiLineVisibleLines))
: kSingleLineTextFill)) /
base_font_size;
metrics.key_font_scale = (metrics.panel_h * kKeyFontRatio) / base_font_size;
metrics.input_pos_local = {metrics.padding_x, metrics.label_h};
metrics.input_size = {metrics.panel_w - metrics.padding_x * 2.0f, metrics.input_h};
metrics.input_pos_screen = {config.window_pos.x + metrics.input_pos_local.x,
config.window_pos.y + metrics.input_pos_local.y};
const float remaining_gap =
metrics.panel_h - (metrics.label_h + metrics.input_h + metrics.predict_h + metrics.keys_h +
metrics.padding_bottom);
metrics.predict_gap = std::max(0.0f, remaining_gap * 0.5f);
metrics.predict_pos = {config.window_pos.x, config.window_pos.y + metrics.label_h +
metrics.input_h + metrics.predict_gap};
metrics.predict_size = {metrics.panel_w * (kPredictW / kPanelBaseW), metrics.predict_h};
metrics.close_pos = {config.window_pos.x + metrics.panel_w - metrics.close_w,
metrics.predict_pos.y};
metrics.close_size = {metrics.close_w, metrics.predict_h};
metrics.kb_pos = {config.window_pos.x + metrics.padding_x,
metrics.predict_pos.y + metrics.predict_h + metrics.predict_gap};
metrics.kb_size = {metrics.panel_w - metrics.padding_x * 2.0f, metrics.keys_h};
metrics.key_h =
(metrics.kb_size.y - metrics.key_gap * static_cast<float>(kKeyRows - 1)) / kKeyRows;
if (metrics.key_h < 8.0f) {
metrics.key_h = 8.0f;
}
return metrics;
}
void DrawImeKeyboardGrid(const ImeKbGridLayout& layout, const ImeKbDrawParams& params,
ImeKbDrawState& state) {
auto* draw = ImGui::GetWindowDrawList();
if (!draw || layout.cols <= 0 || layout.rows <= 0 || layout.size.x <= 0.0f ||
layout.size.y <= 0.0f) {
return;
}
const float key_gap_x = layout.key_gap_x;
const float key_gap_y = layout.key_gap_y;
const float key_h = layout.key_h;
const float key_w = (layout.size.x - key_gap_x * (layout.cols - 1)) / layout.cols;
const auto draw_key = [&](ImVec2 pos, ImVec2 size, ImU32 bg) {
draw->AddRectFilled(pos, {pos.x + size.x, pos.y + size.y}, bg, layout.corner_radius);
draw->AddRect(pos, {pos.x + size.x, pos.y + size.y}, params.key_border,
layout.corner_radius);
};
const auto draw_key_label = [&](ImVec2 pos, ImVec2 size, ImU32 bg, const char* label) {
draw_key(pos, size, bg);
if (label && label[0] != '\0') {
ImVec2 text_size = ImGui::CalcTextSize(label);
ImVec2 text_pos{pos.x + (size.x - text_size.x) * 0.5f,
pos.y + (size.y - text_size.y) * 0.5f};
draw->AddText(text_pos, params.key_text, label);
}
};
constexpr KeySpec blank{1, nullptr, false};
const std::array<KeySpec, kKeyCols> row_default = {blank, blank, blank, blank, blank,
blank, blank, blank, blank, blank};
const std::array<KeySpec, 6> row_space = {
blank, blank, blank, {4, nullptr, false}, blank, {2, nullptr, false},
};
const std::array<KeySpec, 9> row_done = {
blank, blank, blank, blank, blank, blank, blank, blank, {2, nullptr, true}};
auto draw_row = [&](int row_index, const auto& row) {
float x = layout.pos.x;
float y = layout.pos.y + row_index * (key_h + key_gap_y);
for (const auto& key : row) {
const float span_w = key_w * key.span + key_gap_x * (key.span - 1);
ImVec2 pos{x, y};
ImVec2 size{span_w, key_h};
ImU32 bg = params.key_bg;
if (row_index >= layout.rows - 2) {
bg = params.key_bg_alt;
}
if (key.is_done) {
bg = params.key_done;
}
const char* label = key.is_done ? GetEnterLabel(params.enter_label) : key.label;
if (label && label[0] != '\0') {
draw_key_label(pos, size, bg, label);
} else {
draw_key(pos, size, bg);
}
if (key.is_done) {
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton("##ImeDialogDoneKey", size);
if (ImGui::IsItemClicked()) {
state.done_pressed = true;
}
}
x += span_w + key_gap_x;
}
};
draw_row(0, row_default);
draw_row(1, row_default);
draw_row(2, row_default);
draw_row(3, row_default);
draw_row(4, row_space);
draw_row(5, row_done);
}
} // namespace Libraries::Ime

View File

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <imgui.h>
#include "core/libraries/ime/ime_common.h"
namespace Libraries::Ime {
struct ImeViewportMetrics {
ImVec2 size{};
ImVec2 offset{};
float scale_x = 1.0f;
float scale_y = 1.0f;
float ui_scale = 1.0f;
float base_w = 1920.0f;
float base_h = 1080.0f;
};
struct ImeKbGridLayout {
ImVec2 pos{};
ImVec2 size{};
float key_gap_x = 0.0f;
float key_gap_y = 0.0f;
float key_h = 0.0f;
int cols = 10;
int rows = 6;
float corner_radius = 0.0f;
};
struct ImeKbDrawParams {
OrbisImeEnterLabel enter_label = OrbisImeEnterLabel::Default;
ImU32 key_bg = IM_COL32(35, 35, 35, 255);
ImU32 key_bg_alt = IM_COL32(50, 50, 50, 255);
ImU32 key_border = IM_COL32(80, 80, 80, 255);
ImU32 key_done = IM_COL32(30, 90, 170, 255);
ImU32 key_text = IM_COL32(230, 230, 230, 255);
};
struct ImeKbDrawState {
bool done_pressed = false;
};
struct ImePanelMetricsConfig {
float panel_w = 0.0f;
float panel_h = 0.0f;
bool multiline = false;
bool show_title = true;
float base_font_size = 0.0f;
ImVec2 window_pos{};
};
struct ImePanelMetrics {
float panel_w = 0.0f;
float panel_h = 0.0f;
float padding_x = 0.0f;
float padding_bottom = 0.0f;
float label_h = 0.0f;
float input_h = 0.0f;
float predict_h = 0.0f;
float close_w = 0.0f;
float keys_h = 0.0f;
float key_gap = 0.0f;
float key_h = 0.0f;
float corner_radius = 0.0f;
float label_font_scale = 1.0f;
float input_font_scale = 1.0f;
float key_font_scale = 1.0f;
float predict_gap = 0.0f;
ImVec2 input_pos_local{};
ImVec2 input_size{};
ImVec2 input_pos_screen{};
ImVec2 predict_pos{};
ImVec2 predict_size{};
ImVec2 close_pos{};
ImVec2 close_size{};
ImVec2 kb_pos{};
ImVec2 kb_size{};
};
ImeViewportMetrics ComputeImeViewportMetrics(bool use_over2k);
ImePanelMetrics ComputeImePanelMetrics(const ImePanelMetricsConfig& config);
void DrawImeKeyboardGrid(const ImeKbGridLayout& layout, const ImeKbDrawParams& params,
ImeKbDrawState& state);
} // namespace Libraries::Ime

View File

@ -3,6 +3,7 @@
#include <algorithm>
#include <vector>
#include "core/libraries/ime/ime_kb_layout.h"
#include "ime_ui.h"
#include "imgui/imgui_std.h"
@ -10,8 +11,6 @@ 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");
@ -207,26 +206,44 @@ void ImeUi::Draw() {
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);
const bool use_over2k =
(ime_param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT;
const auto viewport = Libraries::Ime::ComputeImeViewportMetrics(use_over2k);
const float scale_x = viewport.scale_x;
const float scale_y = viewport.scale_y;
ImVec2 window_pos = {pos_x, pos_y};
ImVec2 window_size = {500.0f, 100.0f};
u32 panel_req_w = 0;
u32 panel_req_h = 0;
(void)sceImeGetPanelSize(ime_param, &panel_req_w, &panel_req_h);
// SetNextWindowPos(window_pos);
SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
ImVec2 window_size{};
if (panel_req_w > 0 && panel_req_h > 0) {
window_size = {panel_req_w * scale_x, panel_req_h * scale_y};
} else {
window_size = {std::min(std::max(0.0f, viewport.size.x - 40.0f), 640.0f),
std::min(std::max(0.0f, viewport.size.y - 40.0f), 420.0f)};
window_size.x = std::max(window_size.x, 320.0f);
window_size.y = std::max(window_size.y, 240.0f);
}
float pos_x = viewport.offset.x + ime_param->posx * scale_x;
float pos_y = viewport.offset.y + ime_param->posy * scale_y;
if (ime_param->horizontal_alignment == OrbisImeHorizontalAlignment::Center) {
pos_x -= window_size.x * 0.5f;
} else if (ime_param->horizontal_alignment == OrbisImeHorizontalAlignment::Right) {
pos_x -= window_size.x;
}
if (ime_param->vertical_alignment == OrbisImeVerticalAlignment::Center) {
pos_y -= window_size.y * 0.5f;
} else if (ime_param->vertical_alignment == OrbisImeVerticalAlignment::Bottom) {
pos_y -= window_size.y;
}
pos_x = std::clamp(pos_x, viewport.offset.x,
viewport.offset.x + std::max(0.0f, viewport.size.x - window_size.x));
pos_y = std::clamp(pos_y, viewport.offset.y,
viewport.offset.y + std::max(0.0f, viewport.size.y - window_size.y));
SetNextWindowPos({pos_x, pos_y});
SetNextWindowSize(window_size);
SetNextWindowCollapsed(false);
@ -235,40 +252,94 @@ void ImeUi::Draw() {
}
if (Begin("IME##Ime", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoSavedSettings)) {
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) {
DrawPrettyBackground();
const Libraries::Ime::ImePanelMetricsConfig metrics_cfg{
.panel_w = window_size.x,
.panel_h = window_size.y,
.multiline = True(ime_param->option & OrbisImeOption::MULTILINE),
.show_title = false,
.base_font_size = GetFontSize(),
.window_pos = GetWindowPos(),
};
const Libraries::Ime::ImePanelMetrics metrics =
Libraries::Ime::ComputeImePanelMetrics(metrics_cfg);
DrawInputText();
SetCursorPosY(GetCursorPosY() + 10.0f);
SetWindowFontScale(std::max(viewport.ui_scale, metrics.input_font_scale));
DrawInputText(metrics);
const char* button_text;
button_text = "Done##ImeDone";
auto* draw = GetWindowDrawList();
const ImU32 pane_bg = IM_COL32(18, 18, 18, 255);
const ImU32 pane_border = IM_COL32(70, 70, 70, 255);
draw->AddRectFilled(metrics.predict_pos,
{metrics.predict_pos.x + metrics.predict_size.x,
metrics.predict_pos.y + metrics.predict_size.y},
pane_bg, metrics.corner_radius);
draw->AddRect(metrics.predict_pos,
{metrics.predict_pos.x + metrics.predict_size.x,
metrics.predict_pos.y + metrics.predict_size.y},
pane_border, metrics.corner_radius);
PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 1.0f));
PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.35f, 0.35f, 1.0f));
SetCursorScreenPos(metrics.close_pos);
const bool cancel_pressed =
Button("X##ImeClose", {metrics.close_size.x, metrics.close_size.y});
PopStyleColor(3);
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;
SetCursorScreenPos(metrics.kb_pos);
SetCursorPosX(button_start_pos);
if (!accept_armed) {
if (!IsKeyDown(ImGuiKey_Enter) && !IsKeyDown(ImGuiKey_GamepadFaceDown)) {
accept_armed = true;
}
}
const bool allow_key_accept = accept_armed;
if (Button(button_text, BUTTON_SIZE) || (IsKeyPressed(ImGuiKey_Enter))) {
state->SendEnterEvent();
bool accept_pressed =
(allow_key_accept && !metrics_cfg.multiline && IsKeyPressed(ImGuiKey_Enter)) ||
(allow_key_accept && IsKeyPressed(ImGuiKey_GamepadFaceDown));
Libraries::Ime::ImeKbGridLayout kb_layout{};
kb_layout.pos = metrics.kb_pos;
kb_layout.size = metrics.kb_size;
kb_layout.key_gap_x = metrics.key_gap;
kb_layout.key_gap_y = metrics.key_gap;
kb_layout.key_h = metrics.key_h;
kb_layout.cols = 10;
kb_layout.rows = 6;
kb_layout.corner_radius = metrics.corner_radius;
Libraries::Ime::ImeKbDrawParams kb_params{};
kb_params.enter_label = ime_param->enter_label;
kb_params.key_bg_alt = IM_COL32(45, 45, 45, 255);
Libraries::Ime::ImeKbDrawState kb_state{};
SetWindowFontScale(metrics.key_font_scale);
Libraries::Ime::DrawImeKeyboardGrid(kb_layout, kb_params, kb_state);
SetWindowFontScale(metrics.input_font_scale);
if (kb_state.done_pressed) {
accept_pressed = true;
}
SameLine(0.0f, button_spacing);
Dummy({metrics.kb_size.x, metrics.kb_size.y + metrics.padding_bottom});
if (Button("Close##ImeClose", BUTTON_SIZE)) {
if (accept_pressed) {
state->SendEnterEvent();
} else if (cancel_pressed) {
state->SendCloseEvent();
}
SetWindowFontScale(1.0f);
}
End();
first_render = false;
}
void ImeUi::DrawInputText() {
ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f};
SetCursorPosX(20.0f);
void ImeUi::DrawInputText(const ImePanelMetrics& metrics) {
const ImVec2 input_size = metrics.input_size;
SetCursorPos(metrics.input_pos_local);
if (first_render) {
SetKeyboardFocusHere();
}

View File

@ -17,6 +17,7 @@ namespace Libraries::Ime {
class ImeHandler;
class ImeUi;
struct ImePanelMetrics;
class ImeState {
friend class ImeHandler;
@ -57,6 +58,7 @@ class ImeUi : public ImGui::Layer {
const OrbisImeParamExtended* extended_param{};
bool first_render = true;
bool accept_armed = false;
std::mutex draw_mutex;
public:
@ -71,9 +73,9 @@ public:
private:
void Free();
void DrawInputText();
void DrawInputText(const ImePanelMetrics& metrics);
static int InputTextCallback(ImGuiInputTextCallbackData* data);
};
}; // namespace Libraries::Ime
}; // namespace Libraries::Ime

View File

@ -47,6 +47,8 @@ struct SwVersionStruct {
u32 hex_representation;
};
s32 PS4_SYSV_ABI sceKernelGetSystemSwVersion(SwVersionStruct* ret);
struct AuthInfoData {
u64 paid;
u64 caps[4];