diff --git a/CMakeLists.txt b/CMakeLists.txt index b07dcea87d8..15dd8b541bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -507,6 +507,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 diff --git a/src/common/config.cpp b/src/common/config.cpp index fb1181d6279..ee9dd03376c 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -159,6 +159,8 @@ static ConfigEntry isMotionControlsEnabled(true); static ConfigEntry useUnifiedInputConfig(true); static ConfigEntry defaultControllerID(""); static ConfigEntry backgroundControllerInput(false); +static ConfigEntry imeAccessibilityEnabled(false); +static ConfigEntry imeUrlMailShortPanel(false); // Audio static ConfigEntry 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); diff --git a/src/common/config.h b/src/common/config.h index eb2b91f523f..d2fa7390c79 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -142,6 +142,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(); diff --git a/src/core/libraries/ime/ime.cpp b/src/core/libraries/ime/ime.cpp index 258cc61e105..d4f9789d147 100644 --- a/src/core/libraries/ime/ime.cpp +++ b/src/core/libraries/ime/ime.cpp @@ -4,6 +4,7 @@ #include #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(param->option) & ~0x7BFF) { // Basic check for invalid options - LOG_ERROR(Lib_Ime, "Invalid option: {:032b}", static_cast(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(param->type)); - break; - case OrbisImeType::BasicLatin: - *width = 500; // dummy value - *height = 100; // dummy value - LOG_DEBUG(Lib_Ime, "param->type: BasicLatin ({})", static_cast(param->type)); - break; - case OrbisImeType::Url: - *width = 500; // dummy value - *height = 100; // dummy value - LOG_DEBUG(Lib_Ime, "param->type: Url ({})", static_cast(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(param->type)); - break; - case OrbisImeType::Number: - *width = 370; - *height = 402; - LOG_DEBUG(Lib_Ime, "param->type: Number ({})", static_cast(param->type)); - break; - default: - LOG_ERROR(Lib_Ime, "Invalid param->type: ({})", static_cast(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; } diff --git a/src/core/libraries/ime/ime.h b/src/core/libraries/ime/ime.h index b2b4a51ad4d..3e0a3d8a305 100644 --- a/src/core/libraries/ime/ime.h +++ b/src/core/libraries/ime/ime.h @@ -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(); diff --git a/src/core/libraries/ime/ime_common.h b/src/core/libraries/ime/ime_common.h index eafb2ce9dfb..380e6f402b0 100644 --- a/src/core/libraries/ime/ime_common.h +++ b/src/core/libraries/ime/ime_common.h @@ -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; diff --git a/src/core/libraries/ime/ime_dialog.cpp b/src/core/libraries/ime/ime_dialog.cpp index 226570bd6b4..3c21bca1506 100644 --- a/src/core/libraries/ime/ime_dialog.cpp +++ b/src/core/libraries/ime/ime_dialog.cpp @@ -2,10 +2,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include - +#include +#include +#include +#include "common/config.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" #include "ime_dialog.h" #include "ime_dialog_ui.h" @@ -19,299 +23,892 @@ static OrbisImeDialogStatus g_ime_dlg_status = OrbisImeDialogStatus::None; static OrbisImeDialogResult g_ime_dlg_result{}; static ImeDialogState g_ime_dlg_state{}; static ImeDialogUi g_ime_dlg_ui; +static OrbisImeDialogParam g_ime_dlg_param{}; +static OrbisImeParamExtended g_ime_dlg_extended{}; +static bool g_ime_dlg_has_extended = false; +static bool g_ime_dlg_result_committed = false; +static bool g_ime_dlg_sw_version_cached = false; +static u32 g_ime_dlg_sw_version_hex = 0; +static OrbisImeExtKeyboardFilter g_ime_dlg_ext_keyboard_filter = nullptr; +static Libraries::UserService::OrbisUserServiceUserId g_ime_dlg_ext_keyboard_filter_user_id = + Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; +static bool g_ime_dlg_ext_keyboard_filter_registered = false; +static bool g_ime_dlg_ext_keyboard_filter_active = false; +static u32 g_ime_dlg_resource_id = 0; + +static Error ValidateImeDialogParam(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, bool internal); +static Error SetupDialogState(OrbisImeDialogParam* param, OrbisImeParamExtended* extended, + bool internal); +static bool IsValidDialogUserId(s32 user_id, bool internal); +static Error ComputeImeDialogPanelSize(const OrbisImeDialogParam* param, u32* width, u32* height, + bool log); +static Error ComputeImeDialogPanelSizeExtended(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, u32* width, + u32* height, bool log); + +struct ImeDialogClientStub { + bool connected = false; + bool started = false; + u32 user_id = static_cast(Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID); + u32 resource_id = 0; + int dialog_state = 0; +}; -static bool IsValidOption(OrbisImeOption option, OrbisImeType type) { - if (False(~option & (OrbisImeOption::MULTILINE | - OrbisImeOption::NO_AUTO_CAPITALIZATION /* NoAutoCompletion */))) { - return false; +static ImeDialogClientStub* g_ime_dlg_client = nullptr; + +static void DestroyImeDialogClient() { + if (g_ime_dlg_client) { + delete g_ime_dlg_client; + g_ime_dlg_client = nullptr; + } +} + +static ImeDialogClientStub* CreateImeDialogClient() { + auto* client = new (std::nothrow) ImeDialogClientStub{}; + return client; +} + +static void InitImeDialogClient(ImeDialogClientStub* client) { + if (!client) { + return; + } + *client = {}; + client->user_id = static_cast(Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID); +} + +static int ImeDialogClientConnect(ImeDialogClientStub* client, u32 client_type, u32 resource_id, + Libraries::UserService::OrbisUserServiceUserId user_id, + bool internal) { + (void)client_type; + if (!client) { + return static_cast(Error::INTERNAL); + } + if (!IsValidDialogUserId(user_id, internal)) { + return static_cast(Error::INVALID_USER_ID); } + client->connected = true; + client->resource_id = resource_id; + client->user_id = static_cast(user_id); + return 0; +} + +static int ImeDialogClientStart(ImeDialogClientStub* client, const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, u32 user_flags, s64 user_id, + s32 unk1, s32 unk2, bool internal) { + (void)param; + (void)extended; + (void)user_flags; + (void)user_id; + (void)unk1; + (void)unk2; + (void)internal; + if (!client || !client->connected) { + return static_cast(Error::NOT_ACTIVE); + } + client->started = true; + return 0; +} + +static int ImeDialogClientStartFallback(ImeDialogClientStub* client, + const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, u32 user_flags, + s64 user_id) { + return ImeDialogClientStart(client, param, extended, user_flags, user_id, 0, -1, false); +} + +static void ImeDialogClientShutdown(ImeDialogClientStub* client) { + if (!client) { + return; + } + client->started = false; + client->connected = false; + client->dialog_state = 0; +} + +static Error ImeDialogClientDisconnect(ImeDialogClientStub* client) { + if (!client || !client->connected) { + return Error::CONNECTION_FAILED; + } + ImeDialogClientShutdown(client); + return Error::OK; +} - if (True(option & OrbisImeOption::MULTILINE) && type != OrbisImeType::Default && - type != OrbisImeType::BasicLatin) { +static int RegisterExtKeyboardFilter(Libraries::UserService::OrbisUserServiceUserId user_id, + OrbisImeExtKeyboardFilter filter) { + (void)user_id; + (void)filter; + return ORBIS_OK; +} + +static void SetExtKeyboardFilterActive(bool active) { + g_ime_dlg_ext_keyboard_filter_active = active; +} + +static void NotifyExtKeyboardFilterState(bool active) { + if (!g_ime_dlg_ext_keyboard_filter || !g_ime_dlg_ext_keyboard_filter_registered) { + g_ime_dlg_ext_keyboard_filter_active = false; + return; + } + SetExtKeyboardFilterActive(active); +} + +static void ComputeUserFlags(Libraries::UserService::OrbisUserServiceUserId user_id, u32* flags, + s64* out_user) { + u32 local_flags = 0x11; + s64 local_user = 0; + const s32 uid = user_id; + if ((uid + 1) > 1 && uid != 0xff) { + if (uid == 0xfe) { + local_flags = 0; + local_user = 0; + } else { + local_flags = 1; + local_user = uid; + } + } + if (flags) { + *flags = local_flags; + } + if (out_user) { + *out_user = local_user; + } +} + +static int InitDialogInternalWithClient(OrbisImeDialogParam* param, OrbisImeParamExtended* extended, + u32 user_flags, s64 user_value, u32 resource_id, u32 unk1, + u32 unk2, bool connect_returns_connection_failed) { + if (g_ime_dlg_client != nullptr || g_ime_dlg_status != OrbisImeDialogStatus::None) { + return static_cast(Error::BUSY); + } + if (!param) { + return static_cast(Error::INVALID_ADDRESS); + } + + const Error validate_ret = ValidateImeDialogParam(param, extended, true); + if (validate_ret != Error::OK) { + return static_cast(validate_ret); + } + + g_ime_dlg_resource_id = resource_id; + g_ime_dlg_client = CreateImeDialogClient(); + if (!g_ime_dlg_client) { + return static_cast(Error::NO_MEMORY); + } + InitImeDialogClient(g_ime_dlg_client); + + const int connect_ret = + ImeDialogClientConnect(g_ime_dlg_client, 2, g_ime_dlg_resource_id, param->user_id, true); + if (connect_ret != 0) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + if (connect_returns_connection_failed) { + return static_cast(Error::CONNECTION_FAILED); + } + return connect_ret; + } + + const int start_ret = + ImeDialogClientStart(g_ime_dlg_client, param, extended, user_flags, user_value, + static_cast(unk1), static_cast(unk2), true); + if (start_ret != 0) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return start_ret; + } + + const Error setup_ret = SetupDialogState(param, extended, true); + if (setup_ret != Error::OK) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return static_cast(setup_ret); + } + + return static_cast(Error::OK); +} + +static void CommitDialogResultIfNeeded() { + if (g_ime_dlg_result_committed) { + return; + } + if (g_ime_dlg_status != OrbisImeDialogStatus::Finished) { + return; + } + + g_ime_dlg_state.CallTextFilter(); + const bool restore_original = (g_ime_dlg_result.endstatus != OrbisImeDialogEndStatus::Ok); + g_ime_dlg_state.CopyTextToOrbisBuffer(restore_original); + g_ime_dlg_result_committed = true; + LOG_DEBUG(Lib_ImeDialog, "Committed result (restore_original={})", restore_original); +} + +static bool UseOver2kCoordinates(const OrbisImeDialogParam& param) { + return True(param.option & OrbisImeOption::USE_OVER_2K_COORDINATES); +} + +static float ToInternalCoord(float value, const OrbisImeDialogParam& param) { + return UseOver2kCoordinates(param) ? value : (value * 2.0f); +} + +static float FromInternalCoord(float value, const OrbisImeDialogParam& param) { + return UseOver2kCoordinates(param) ? value : std::round(value * 0.5f); +} + +static float ClampInternalX(float value) { + if (value < 0.0f) { + return 0.0f; + } + if (value >= 3840.0f) { + return 3839.0f; + } + return value; +} + +static float ClampInternalY(float value) { + if (value < 0.0f) { + return 0.0f; + } + if (value >= 2160.0f) { + return 2159.0f; + } + return value; +} + +static u32 GetCompiledSdkVersion() { + if (!g_ime_dlg_sw_version_cached) { + Libraries::Kernel::SwVersionStruct sw{}; + sw.struct_size = sizeof(sw); + if (Libraries::Kernel::sceKernelGetSystemSwVersion(&sw) == ORBIS_OK) { + g_ime_dlg_sw_version_hex = sw.hex_representation; + } + g_ime_dlg_sw_version_cached = true; + } + + s32 ver = 0; + if (Libraries::Kernel::sceKernelGetCompiledSdkVersion(&ver) != ORBIS_OK) { + return 0; + } + return static_cast(ver); +} + +static u32 GetImeDialogOptionMask() { + const u32 sdk = GetCompiledSdkVersion(); + u32 uVar8 = 0x80068ff; + if (sdk > 0x14fffff) { + uVar8 = 0x69ff; + } + u32 uVar5 = uVar8 & 0x80061ff; + if (sdk > 0x174ffff) { + uVar5 = uVar8; + } + uVar8 = uVar5 & 0x80049ff; + if (sdk > 0x34fffff) { + uVar8 = uVar5; + } + uVar5 = uVar8 & 0x80029ff; + if (sdk > 0x3ffffff) { + uVar5 = uVar8; + } + return uVar5; +} + +static u64 GetImeDialogLanguageMask() { + const u32 sdk = GetCompiledSdkVersion(); + u64 uVar3 = (sdk > 0x1ffffff) ? (0x1000000ULL + 0x3fe1fffffULL) : 0x3fe1fffffULL; + u64 uVar7 = uVar3 & 0x3fd1fffffULL; + if (sdk > 0x24fffff) { + uVar7 = uVar3; + } + u64 uVar8 = uVar7 & 0x2031fffffULL; + if (sdk > 0x4ffffff) { + uVar8 = uVar7; + } + uVar7 = uVar8 & 0x1ff1fffffULL; + if (sdk > 0xfffffff) { + uVar7 = uVar8; + } + return uVar7; +} + +static u32 GetImeDialogOptionMaskInternal() { + const u32 sdk = GetCompiledSdkVersion(); + u32 mask = (sdk > 0x14fffff) ? 0xf7f06fff : 0xff706eff; + + // SDK < 0x1700000 masks some bits, otherwise ORs 0x08600000 + u32 tmp = (sdk < 0x1700000) ? (mask & 0xffe06bff) : (mask | 0x08600000); + u32 tmp2 = (sdk > 0x174ffff) ? tmp : (tmp & 0xfff067ff); + u32 tmp3 = (sdk > 0x34fffff) ? tmp2 : (tmp2 & 0xfff04fff); + u32 tmp4 = (sdk > 0x3ffffff) ? tmp3 : (tmp3 & 0xfff02fff); + return tmp4; +} + +static u64 GetImeDialogLanguageMaskInternal() { + const u32 sdk = GetCompiledSdkVersion(); + const u64 base = (sdk > 0x1ffffff ? 0x1000000ULL : 0ULL) + 0x303fe1fffffULL; + u64 mask = (sdk > 0x24fffff) ? base : (base & 0x303fd1fffffULL); + mask = (sdk > 0x4ffffff) ? mask : (mask & 0x302031fffffULL); + mask = (sdk > 0xfffffff) ? mask : (mask & 0x301ff1fffffULL); + return mask; +} + +static bool IsValidDialogExtOption_2a70(u32 option, u32 sdk) { + u32 mask = (sdk < 0x1560000) ? 0x41df : 0x4fdf; + if ((option & 0x4080) == 0x4000) { return false; } + u32 allow = (sdk > 0x5ffffff) ? mask : (mask & 0x0fdf); + return (~allow & option) == 0; +} - if (True(option & OrbisImeOption::NO_AUTO_CAPITALIZATION /* NoAutoCompletion */) && - type != OrbisImeType::Number && type != OrbisImeType::BasicLatin) { +static bool IsValidDialogExtOption_00bd0(u32 option, u32 sdk) { + u32 mask = (sdk < 0x1560000) ? 0x71df : 0x7fdf; + u32 allow = (sdk > 0x24fffff) ? mask : (mask & 0x4fdf); + if (((option & 0x3000) == 0x2000) || ((option & 0x4080) == 0x4000)) { return false; } + allow = (sdk > 0x5ffffff) ? allow : (allow & 0x3fdf); + return (~allow & option) == 0; +} +static bool IsValidDialogExtOption(u32 option) { + const u32 sdk = GetCompiledSdkVersion(); + if (sdk < 0x1500000) { + return (option & 0xffffff20U) == 0; + } + if (sdk < 0x2500000) { + return IsValidDialogExtOption_2a70(option, sdk); + } + return IsValidDialogExtOption_00bd0(option, sdk); +} + +static bool IsValidDialogUserId(s32 user_id, bool internal) { + const u32 sdk = GetCompiledSdkVersion(); + const u32 uid_u = static_cast(user_id); + + if (sdk < 0x1500000) { + if ((uid_u + 1U) < 2U || (uid_u - 0xfeU) < 2U) { + return true; + } + // We cannot query registered users (stubbed), allow non-negative ids. + return user_id >= 0; + } + + if (!internal) { + if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID || + user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_SYSTEM) { + return false; + } + if (user_id == 0xfe) { + return true; + } + } else { + if ((uid_u + 1U) < 2U || (uid_u - 0xfeU) < 2U) { + return true; + } + } + + return user_id >= 0; +} + +static bool IsAllZero(const s8* data, size_t size) { + for (size_t i = 0; i < size; ++i) { + if (data[i] != 0) { + return false; + } + } return true; } +static Error ValidateImeDialogParam(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, bool internal) { + if (!param) { + return Error::INVALID_ADDRESS; + } + + const u32 type = static_cast(param->type); + if (!internal) { + if (type > static_cast(OrbisImeType::Number)) { + return Error::INVALID_TYPE; + } + const u32 option_mask = GetImeDialogOptionMask(); + if ((static_cast(param->option) & (option_mask ^ 0xfffffdffU)) != 0) { + return Error::INVALID_OPTION; + } + const u64 lang_mask = GetImeDialogLanguageMask(); + if ((~lang_mask & static_cast(param->supported_languages)) != 0) { + return Error::INVALID_SUPPORTED_LANGUAGES; + } + } else { + if (type > static_cast(OrbisImeType::Number) && type != 0x100 && type != 0x101) { + return Error::INVALID_TYPE; + } + const u32 option_mask = GetImeDialogOptionMaskInternal(); + if ((~option_mask & static_cast(param->option)) != 0) { + return Error::INVALID_OPTION; + } + const u64 lang_mask = GetImeDialogLanguageMaskInternal(); + if ((~lang_mask & static_cast(param->supported_languages)) != 0) { + return Error::INVALID_SUPPORTED_LANGUAGES; + } + } + + const u32 sdk = GetCompiledSdkVersion(); + if (sdk < 0x1500000) { + if (param->posx < 0.0f || param->posx >= 1920.0f) { + return Error::INVALID_POSX; + } + if (param->posy < 0.0f || param->posy >= 1080.0f) { + return Error::INVALID_POSY; + } + } else { + if (param->posx < 0.0f || + param->posx >= + MAX_X_POSITIONS[False(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES)]) { + return Error::INVALID_POSX; + } + if (param->posy < 0.0f || + param->posy >= + MAX_Y_POSITIONS[False(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES)]) { + return Error::INVALID_POSY; + } + } + + if (static_cast(param->horizontal_alignment) >= 3) { + return Error::INVALID_HORIZONTALIGNMENT; + } + if (static_cast(param->vertical_alignment) >= 3) { + return Error::INVALID_VERTICALALIGNMENT; + } + + const u32 option = static_cast(param->option); + if ((option & 0x5U) == 0x5U) { + return Error::INVALID_PARAM; + } + if ((option & 0x4U) != 0) { + if (!(type > static_cast(OrbisImeType::Mail) || + type == static_cast(OrbisImeType::BasicLatin))) { + return Error::INVALID_PARAM; + } + } + if ((option & 0x1U) != 0) { + if (!(type <= static_cast(OrbisImeType::BasicLatin) || type >= 0x100)) { + return Error::INVALID_PARAM; + } + } + + if (!IsValidDialogUserId(param->user_id, internal)) { + return Error::INVALID_USER_ID; + } + if (!internal && sdk < 0x1500000 && + param->user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return Error::INVALID_USER_ID; + } + + if (!IsAllZero(param->reserved, sizeof(param->reserved))) { + return Error::INVALID_RESERVED; + } + + if (param->input_text_buffer == nullptr) { + return Error::INVALID_INPUT_TEXT_BUFFER; + } + + if (extended) { + if (static_cast(extended->priority) > 3) { + return Error::INVALID_EXTENDED; + } + if (!IsValidDialogExtOption(static_cast(extended->option))) { + return Error::INVALID_EXTENDED; + } + if (!IsAllZero(extended->reserved, sizeof(extended->reserved))) { + return Error::INVALID_EXTENDED; + } + if (sdk < 0x1560000) { + if (extended->ext_keyboard_filter != nullptr) { + return Error::INVALID_EXTENDED; + } + if (static_cast(extended->disable_device) != 0) { + return Error::INVALID_EXTENDED; + } + if (extended->ext_keyboard_mode != 0) { + return Error::INVALID_EXTENDED; + } + } else { + if ((extended->ext_keyboard_mode & 0xe3fffffcU) != 0) { + return Error::INVALID_EXTENDED; + } + } + if (static_cast(extended->disable_device) > 7) { + return Error::INVALID_EXTENDED; + } + } + + return Error::OK; +} + Error PS4_SYSV_ABI sceImeDialogAbort() { - if (g_ime_dlg_status == OrbisImeDialogStatus::None) { + LOG_INFO(Lib_ImeDialog, "Abort called (status={}, client_state={})", + static_cast(g_ime_dlg_status), + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { LOG_INFO(Lib_ImeDialog, "IME dialog not in use"); return Error::DIALOG_NOT_IN_USE; } - if (g_ime_dlg_status != OrbisImeDialogStatus::Running) { - LOG_INFO(Lib_ImeDialog, "IME dialog not running"); - return Error::DIALOG_NOT_RUNNING; - } - g_ime_dlg_status = OrbisImeDialogStatus::Finished; g_ime_dlg_result.endstatus = OrbisImeDialogEndStatus::Aborted; - + CommitDialogResultIfNeeded(); + if (g_ime_dlg_client) { + g_ime_dlg_client->dialog_state = 5; + } return Error::OK; } Error PS4_SYSV_ABI sceImeDialogForceClose() { - LOG_INFO(Lib_ImeDialog, "called"); - if (g_ime_dlg_status == OrbisImeDialogStatus::None) { + LOG_INFO(Lib_ImeDialog, "ForceClose called (status={}, client_state={})", + static_cast(g_ime_dlg_status), + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { LOG_INFO(Lib_ImeDialog, "IME dialog not in use"); return Error::DIALOG_NOT_IN_USE; } + const Error disconnect_ret = ImeDialogClientDisconnect(g_ime_dlg_client); + + if (g_ime_dlg_ext_keyboard_filter_active) { + NotifyExtKeyboardFilterState(false); + } g_ime_dlg_status = OrbisImeDialogStatus::None; g_ime_dlg_ui = ImeDialogUi(); g_ime_dlg_state = ImeDialogState(); - - return Error::OK; + g_ime_dlg_param = {}; + g_ime_dlg_extended = {}; + g_ime_dlg_has_extended = false; + g_ime_dlg_result_committed = false; + g_ime_dlg_ext_keyboard_filter = nullptr; + g_ime_dlg_ext_keyboard_filter_user_id = + Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + g_ime_dlg_ext_keyboard_filter_registered = false; + g_ime_dlg_ext_keyboard_filter_active = false; + g_ime_dlg_resource_id = 0; + DestroyImeDialogClient(); + + return disconnect_ret; } Error PS4_SYSV_ABI sceImeDialogForTestFunction() { return Error::INTERNAL; } -int PS4_SYSV_ABI sceImeDialogGetCurrentStarState() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogGetCurrentStarState(s64 param_1) { + LOG_INFO(Lib_ImeDialog, "GetCurrentStarState called (client_state={}, addr=0x{:X})", + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1, static_cast(param_1)); + if (!g_ime_dlg_client) { + return static_cast(Error::DIALOG_NOT_IN_USE); + } + if (param_1 == 0) { + return static_cast(Error::INVALID_ADDRESS); + } + if (g_ime_dlg_client->dialog_state == 4 || g_ime_dlg_client->dialog_state == 5) { + return static_cast(Error::IME_SUSPENDING); + } + auto* out_state = reinterpret_cast(param_1); + *out_state = 0; + LOG_DEBUG(Lib_ImeDialog, "GetCurrentStarState -> 0"); + return static_cast(Error::OK); } -int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm(OrbisImePositionAndForm* posForm) { + LOG_INFO(Lib_ImeDialog, "GetPanelPositionAndForm called (client_state={})", + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { + return static_cast(Error::DIALOG_NOT_IN_USE); + } + if (!posForm) { + return static_cast(Error::INVALID_ADDRESS); + } + *posForm = {}; + posForm->type = OrbisImePanelType::Dialog; + posForm->posx = FromInternalCoord(g_ime_dlg_param.posx, g_ime_dlg_param); + posForm->posy = FromInternalCoord(g_ime_dlg_param.posy, g_ime_dlg_param); + posForm->horizontal_alignment = g_ime_dlg_param.horizontal_alignment; + posForm->vertical_alignment = g_ime_dlg_param.vertical_alignment; + + OrbisImeDialogParam size_param = g_ime_dlg_param; + size_param.option = + static_cast(static_cast(size_param.option) | + static_cast(OrbisImeOption::USE_OVER_2K_COORDINATES)); + u32 width = 0; + u32 height = 0; + if (g_ime_dlg_has_extended) { + (void)ComputeImeDialogPanelSizeExtended(&size_param, &g_ime_dlg_extended, &width, &height, + false); + } else { + (void)ComputeImeDialogPanelSize(&size_param, &width, &height, false); + } + + if (!UseOver2kCoordinates(g_ime_dlg_param)) { + posForm->width = + static_cast(FromInternalCoord(static_cast(width), g_ime_dlg_param)); + posForm->height = + static_cast(FromInternalCoord(static_cast(height), g_ime_dlg_param)); + } else { + posForm->width = width; + posForm->height = height; + } + LOG_INFO(Lib_ImeDialog, "GetPanelPositionAndForm: type={}, pos=({}, {}), size={}x{}", + static_cast(posForm->type), posForm->posx, posForm->posy, posForm->width, + posForm->height); + return static_cast(Error::OK); } Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u32* width, u32* height) { - LOG_INFO(Lib_ImeDialog, "called"); + return ComputeImeDialogPanelSize(param, width, height, true); +} + +static Error ComputeImeDialogPanelSize(const OrbisImeDialogParam* param, u32* width, u32* height, + bool log) { + if (log) { + LOG_INFO(Lib_ImeDialog, "called"); + } + if (!param) { + return Error::INVALID_ADDRESS; + } if (!width || !height) { return Error::INVALID_ADDRESS; } - switch (param->type) { - case OrbisImeType::Default: - case OrbisImeType::BasicLatin: - case OrbisImeType::Url: - case OrbisImeType::Mail: - *width = 500; // original: 793 - if (True(param->option & OrbisImeOption::MULTILINE)) { - *height = 300; // original: 576 - } else { - *height = 150; // original: 476 - } - break; - case OrbisImeType::Number: - *width = 370; - *height = 470; - break; - default: - LOG_ERROR(Lib_ImeDialog, "Unknown OrbisImeType: {}", (u32)param->type); - return Error::INVALID_PARAM; + if (static_cast(param->type) > static_cast(OrbisImeType::Number)) { + LOG_ERROR(Lib_ImeDialog, "Unknown OrbisImeType: {}", static_cast(param->type)); + return Error::INVALID_TYPE; + } + + u32 option = static_cast(param->option); + const u32 option_mask = GetImeDialogOptionMask(); + const u32 invalid_bits = option & (option_mask ^ 0xfffffdffU); + if (invalid_bits != 0) { + if (log) { + LOG_ERROR(Lib_ImeDialog, "Invalid option: {:032b}", option); + return Error::INVALID_OPTION; + } + LOG_DEBUG(Lib_ImeDialog, "Masking invalid option bits: {:032b}", invalid_bits); + const u32 over2k_bit = static_cast(OrbisImeOption::USE_OVER_2K_COORDINATES); + const u32 preserve_over2k = option & over2k_bit; + option = (option & ~invalid_bits) | preserve_over2k; } + const u64 lang_mask = GetImeDialogLanguageMask(); + if ((~lang_mask & static_cast(param->supported_languages)) != 0) { + if (log) { + LOG_ERROR(Lib_ImeDialog, "Invalid supported_languages: {:064b}", + static_cast(param->supported_languages)); + return Error::INVALID_SUPPORTED_LANGUAGES; + } + LOG_DEBUG(Lib_ImeDialog, "Masking invalid supported_languages bits"); + } + + const u32 sdk = GetCompiledSdkVersion(); + if (param->type == OrbisImeType::Number) { + *width = 0x172; + u32 h = 0x1d6; + if (sdk > 0x16fffff) { + h = 0x20a - (sdk < 0x2000000 ? 1U : 0U); + } + *height = h; + } else { + u32 opt = option; + if (param->type != OrbisImeType::BasicLatin) { + if ((opt & 0xc0000004) != 4) { + *width = 0x319; + *height = (opt & 1) ? 0x274 : 0x210; + goto done; + } + } + *width = 0x319; + if ((opt & 1) == 0) { + *height = (sdk > 0x16fffff) ? 0x210 : 0x1dc; + } else { + *height = (sdk > 0x16fffff) ? 0x274 : 0x240; + } + } +done: + if ((option & 0x4000) != 0) { + *width <<= 1; + *height <<= 1; + } + if (log) { + LOG_DEBUG(Lib_ImeDialog, "PanelSize: type={}, option=0x{:X}, sdk=0x{:X}, size={}x{}", + static_cast(param->type), option, sdk, *width, *height); + } return Error::OK; } Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param, const OrbisImeParamExtended* extended, u32* width, u32* height) { + return ComputeImeDialogPanelSizeExtended(param, extended, width, height, true); +} + +static Error ComputeImeDialogPanelSizeExtended(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, u32* width, + u32* height, bool log) { if (!param || !width || !height) { return Error::INVALID_ADDRESS; } - // Check parameter bounds - if (static_cast(param->type) > 4) { - return Error::INVALID_ARG; + if (static_cast(param->type) > static_cast(OrbisImeType::Number)) { + return Error::INVALID_TYPE; } - - if (extended) { - // Check panel priority for full panel mode (Accent = 3) - if (extended->priority == OrbisImePanelPriority::Accent) { - // Full panel mode - return maximum size - if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != - OrbisImeOption::DEFAULT) { - *width = 2560; // For 4K/5K displays - *height = 1440; - } else { - *width = 1920; - *height = 1080; - } - LOG_DEBUG(Lib_ImeDialog, "Full panel mode: width={}, height={}", *width, *height); - return Error::OK; + u32 option = static_cast(param->option); + const u32 option_mask = GetImeDialogOptionMask(); + const u32 invalid_bits = option & (option_mask ^ 0xfffffdffU); + if (invalid_bits != 0) { + if (log) { + return Error::INVALID_OPTION; + } + const u32 over2k_bit = static_cast(OrbisImeOption::USE_OVER_2K_COORDINATES); + const u32 preserve_over2k = option & over2k_bit; + option = (option & ~invalid_bits) | preserve_over2k; + } + const u64 lang_mask = GetImeDialogLanguageMask(); + if ((~lang_mask & static_cast(param->supported_languages)) != 0) { + if (log) { + return Error::INVALID_SUPPORTED_LANGUAGES; } } - // First get the base panel size from the basic function - Error result = sceImeDialogGetPanelSize(param, width, height); - if (result != Error::OK) { - return result; + if (!extended) { + return sceImeDialogGetPanelSize(param, width, height); } - // Adjust based on IME type - switch (param->type) { - case OrbisImeType::Default: - case OrbisImeType::BasicLatin: - case OrbisImeType::Url: - case OrbisImeType::Mail: - // Standard IME types - if ((param->option & OrbisImeOption::PASSWORD) != OrbisImeOption::DEFAULT) { - *height = *height + 20; - } - if ((param->option & OrbisImeOption::MULTILINE) != OrbisImeOption::DEFAULT) { - *height = *height * 3 / 2; + const u32 sdk = GetCompiledSdkVersion(); + if (sdk > 0x16fffff) { + if (!IsValidDialogExtOption(static_cast(extended->option))) { + return Error::INVALID_EXTENDED; } - break; - - case OrbisImeType::Number: - *width = *width * 3 / 4; - *height = *height * 2 / 3; - break; - - default: - // Unknown type, use default size - break; } - // Apply extended options if provided - if (extended) { - // Handle extended option flags - if ((extended->option & OrbisImeExtOption::PRIORITY_FULL_WIDTH) != - OrbisImeExtOption::DEFAULT) { - // Full width priority - bool use_2k = (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != - OrbisImeOption::DEFAULT; - *width = use_2k ? 1200 : 800; - LOG_DEBUG(Lib_ImeDialog, "Full width priority: width={}", *width); - } - - if ((extended->option & OrbisImeExtOption::PRIORITY_FIXED_PANEL) != - OrbisImeExtOption::DEFAULT) { - // Fixed panel size - *width = 600; - *height = 400; - LOG_DEBUG(Lib_ImeDialog, "Fixed panel: width={}, height={}", *width, *height); - } - - switch (extended->priority) { - case OrbisImePanelPriority::Alphabet: - *width = 600; - *height = 400; - break; - - case OrbisImePanelPriority::Symbol: - *width = 500; - *height = 300; - break; - - case OrbisImePanelPriority::Accent: - // Already handled - break; - - case OrbisImePanelPriority::Default: - default: - // Use calculated sizes - break; - } - - if ((extended->option & OrbisImeExtOption::INIT_EXT_KEYBOARD_MODE) != - OrbisImeExtOption::DEFAULT) { - if (extended->ext_keyboard_mode != 0) { - // Check for high-res mode flags - if ((extended->ext_keyboard_mode & - static_cast( - OrbisImeInitExtKeyboardMode::INPUT_METHOD_STATE_FULL_WIDTH)) != 0) { - *width = *width * 5 / 4; - } - - // Check for format characters enabled - if ((extended->ext_keyboard_mode & - static_cast( - OrbisImeInitExtKeyboardMode::ENABLE_FORMAT_CHARACTERS)) != 0) { - *height = *height + 30; - } - } + bool accessibility = false; + const u32 ext_opt = static_cast(extended->option); + if ((ext_opt & static_cast(OrbisImeExtOption::ENABLE_ACCESSIBILITY)) != 0) { + if ((ext_opt & static_cast(OrbisImeExtOption::ACCESSIBILITY_PANEL_FORCED)) != 0) { + accessibility = true; + } else { + // Emulate system accessibility setting (LLE FUN_01005a50 path). + accessibility = Config::getImeAccessibilityEnabled(); } + } - // Check for accessibility mode - if ((extended->option & OrbisImeExtOption::ENABLE_ACCESSIBILITY) != - OrbisImeExtOption::DEFAULT) { - *width = *width * 5 / 4; // 25% larger for accessibility - *height = *height * 5 / 4; - LOG_DEBUG(Lib_ImeDialog, "Accessibility mode: width={}, height={}", *width, *height); + if (accessibility) { + *width = 0x780; + *height = 0x438; + if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT) { + *width <<= 1; + *height <<= 1; } - - // Check for forced accessibility panel - if ((extended->option & OrbisImeExtOption::ACCESSIBILITY_PANEL_FORCED) != - OrbisImeExtOption::DEFAULT) { - *width = 800; - *height = 600; - LOG_DEBUG(Lib_ImeDialog, "Forced accessibility panel: width={}, height={}", *width, - *height); + if (log) { + LOG_DEBUG(Lib_ImeDialog, "PanelSizeExt: type={}, option=0x{:X}, sdk=0x{:X}, size={}x{}", + static_cast(param->type), option, sdk, *width, *height); } + return Error::OK; } - if ((param->option & static_cast(0x8)) != OrbisImeOption::DEFAULT) { //? - *width *= 2; - *height *= 2; - LOG_DEBUG(Lib_ImeDialog, "Size mode: width={}, height={}", *width, *height); - } - - // Adjust for supported languages if specified - if (param->supported_languages != static_cast(0)) { - // Check if CJK languages are supported (need larger panel) - OrbisImeLanguage cjk_mask = OrbisImeLanguage::JAPANESE | OrbisImeLanguage::KOREAN | - OrbisImeLanguage::SIMPLIFIED_CHINESE | - OrbisImeLanguage::TRADITIONAL_CHINESE; - - if ((param->supported_languages & cjk_mask) != static_cast(0)) { - *width = *width * 5 / 4; // 25% wider for CJK input - *height = *height * 6 / 5; // 20% taller - LOG_DEBUG(Lib_ImeDialog, "CJK language support: width={}, height={}", *width, *height); + const bool hide_keypanel_for_ext = + (param->option & OrbisImeOption::EXT_KEYBOARD) != OrbisImeOption::DEFAULT && + (extended->option & OrbisImeExtOption::HIDE_KEYPANEL_IF_EXT_KEYBOARD) != + OrbisImeExtOption::DEFAULT; + + const u32 type = static_cast(param->type); + const bool multiline = (option & 1U) != 0; + + auto use_short_url_mail_height = [&](u32 ime_type) { + // LLE: (SVar1 & ~BASIC_LATIN) == (URL | bVar15), with bVar15 default true. + // When bVar15 is true, this condition is false for Url/Mail; false enables short heights. + const bool bVar15 = !Config::getImeUrlMailShortPanel(); + const u32 masked_type = ime_type & ~static_cast(OrbisImeType::BasicLatin); + const u32 url_or_mail = static_cast(OrbisImeType::Url) | (bVar15 ? 1U : 0U); + return masked_type == url_or_mail; + }; + + if (type == static_cast(OrbisImeType::Number)) { + *width = 0x172; + if (!hide_keypanel_for_ext) { + if (sdk > 0x16fffff) { + *height = (sdk < 0x2000000) ? 0x209 : 0x20a; + } else { + *height = 0x1d6; + } + } else { + if (sdk < 0x1700000) { + *height = 0x1d6; + } else { + *height = (sdk < 0x2000000) ? 0x65 : 0x66; + } } - - // Check if Arabic is supported (right-to-left layout) - if ((param->supported_languages & OrbisImeLanguage::ARABIC) != - static_cast(0)) { - *width = *width * 11 / 10; // 10% wider for Arabic - LOG_DEBUG(Lib_ImeDialog, "Arabic language support: width={}", *width); + } else { + const bool basic_latin_branch = + (type == static_cast(OrbisImeType::BasicLatin)) || ((option & 0xc0000004U) == 4U); + + if (basic_latin_branch) { + *width = 0x319; + if (!multiline) { + if (!hide_keypanel_for_ext) { + *height = (sdk > 0x16fffff) ? 0x210 : 0x1dc; + } else { + *height = (sdk < 0x1700000) ? 0x66 : 0x67; + } + } else { + if (!hide_keypanel_for_ext) { + *height = (sdk > 0x16fffff) ? 0x274 : 0x240; + } else { + *height = (sdk < 0x1700000) ? 0x10a : 0xcb; + } + } + } else { + *width = 0x319; + if (!multiline) { + if (hide_keypanel_for_ext) { + const bool short_url_mail = use_short_url_mail_height(type); + if (short_url_mail) { + *height = (sdk < 0x1700000) ? 0x66 : 0x67; + } else { + *height = (sdk < 0x1700000) ? 0xa6 : 0xa8; + } + } else { + *height = 0x210; + } + } else { + if (hide_keypanel_for_ext) { + const bool short_url_mail = use_short_url_mail_height(type); + if (short_url_mail) { + *height = (sdk < 0x1700000) ? 0xca : 0xcb; + } else { + *height = (sdk < 0x1700000) ? 0x10a : 0x10c; + } + } else { + *height = 0x274; + } + } } } - // Ensure minimum sizes - const uint32_t min_width = 200; - const uint32_t min_height = 100; - if (*width < min_width) - *width = min_width; - if (*height < min_height) - *height = min_height; - - // Ensure maximum sizes (don't exceed screen bounds) - bool use_2k_coords = - (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT; - const uint32_t max_width = use_2k_coords ? 2560 : 1920; - const uint32_t max_height = use_2k_coords ? 1440 : 1080; - if (*width > max_width) - *width = max_width; - if (*height > max_height) - *height = max_height; - - // Check for fixed position option - if ((param->option & OrbisImeOption::FIXED_POSITION) != OrbisImeOption::DEFAULT) { - if (*width > 800) - *width = 800; - if (*height > 600) - *height = 600; + if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT) { + *width <<= 1; + *height <<= 1; } - LOG_DEBUG(Lib_ImeDialog, "Final panel size: width={}, height={}", *width, *height); + if (log) { + LOG_DEBUG(Lib_ImeDialog, "PanelSizeExt: type={}, option=0x{:X}, sdk=0x{:X}, size={}x{}", + static_cast(param->type), option, sdk, *width, *height); + } return Error::OK; } Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result) { - if (g_ime_dlg_status == OrbisImeDialogStatus::None) { + LOG_INFO(Lib_ImeDialog, "GetResult called (status={}, client_state={})", + static_cast(g_ime_dlg_status), + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { LOG_INFO(Lib_ImeDialog, "IME dialog is not running"); return Error::DIALOG_NOT_IN_USE; } @@ -321,32 +918,155 @@ Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result) { return Error::INVALID_ADDRESS; } - result->endstatus = g_ime_dlg_result.endstatus; + for (size_t i = 0; i < sizeof(result->reserved); ++i) { + if (result->reserved[i] != 0) { + LOG_INFO(Lib_ImeDialog, "result->reserved not zeroed"); + return Error::INVALID_RESERVED; + } + } + + if (g_ime_dlg_client && g_ime_dlg_client->dialog_state == 4) { + return Error::DIALOG_NOT_FINISHED; + } if (g_ime_dlg_status == OrbisImeDialogStatus::Running) { return Error::DIALOG_NOT_FINISHED; } - g_ime_dlg_state.CopyTextToOrbisBuffer(); + if (g_ime_dlg_status == OrbisImeDialogStatus::Finished && g_ime_dlg_client) { + g_ime_dlg_client->dialog_state = 5; + } + + CommitDialogResultIfNeeded(); + result->endstatus = g_ime_dlg_result.endstatus; + LOG_INFO(Lib_ImeDialog, "GetResult -> endstatus={}", static_cast(result->endstatus)); return Error::OK; } OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus() { + LOG_INFO(Lib_ImeDialog, "GetStatus called (status={}, client_state={})", + static_cast(g_ime_dlg_status), + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { + LOG_DEBUG(Lib_ImeDialog, "GetStatus -> None (no client)"); + return OrbisImeDialogStatus::None; + } + + if (g_ime_dlg_client->dialog_state == 5) { + g_ime_dlg_status = OrbisImeDialogStatus::Finished; + } + if (g_ime_dlg_status == OrbisImeDialogStatus::Running) { g_ime_dlg_state.CallTextFilter(); + } else if (g_ime_dlg_status == OrbisImeDialogStatus::Finished) { + CommitDialogResultIfNeeded(); + } + + if (g_ime_dlg_client) { + if (g_ime_dlg_status == OrbisImeDialogStatus::Running) { + g_ime_dlg_client->dialog_state = 4; + } else if (g_ime_dlg_status == OrbisImeDialogStatus::Finished) { + g_ime_dlg_client->dialog_state = 5; + } + } + + if (g_ime_dlg_status == OrbisImeDialogStatus::Running && g_ime_dlg_ext_keyboard_filter && + g_ime_dlg_ext_keyboard_filter_registered && !g_ime_dlg_ext_keyboard_filter_active) { + NotifyExtKeyboardFilterState(true); + } else if (g_ime_dlg_status != OrbisImeDialogStatus::Running && + g_ime_dlg_ext_keyboard_filter_active) { + NotifyExtKeyboardFilterState(false); } + LOG_DEBUG(Lib_ImeDialog, "GetStatus -> {}", static_cast(g_ime_dlg_status)); return g_ime_dlg_status; } -Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended) { - LOG_INFO(Lib_ImeDialog, "called, param={}, extended={}", static_cast(param), - static_cast(extended)); +static Error SetupDialogState(OrbisImeDialogParam* param, OrbisImeParamExtended* extended, + bool internal) { + if (!param) { + return Error::INVALID_ADDRESS; + } + + g_ime_dlg_resource_id = 0; + g_ime_dlg_ext_keyboard_filter = nullptr; + g_ime_dlg_ext_keyboard_filter_user_id = + Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + g_ime_dlg_ext_keyboard_filter_registered = false; + if (param->max_text_length == 0 || param->max_text_length > ORBIS_IME_MAX_TEXT_LENGTH) { + LOG_ERROR(Lib_ImeDialog, "sceImeDialogInit: invalid max_text_length={}", + param->max_text_length); + return Error::INVALID_MAX_TEXT_LENGTH; + } + + g_ime_dlg_result = {}; + g_ime_dlg_result_committed = false; + + OrbisImeDialogParam local_param = *param; + const u32 sdk = GetCompiledSdkVersion(); + if (!internal && sdk < 0x1500000) { + local_param.option = + static_cast(static_cast(local_param.option) & 0x26ffffffU); + } + local_param.posx = ClampInternalX(ToInternalCoord(local_param.posx, local_param)); + local_param.posy = ClampInternalY(ToInternalCoord(local_param.posy, local_param)); + g_ime_dlg_param = local_param; + + OrbisImeParamExtended* ext_ptr = nullptr; + if (extended) { + g_ime_dlg_extended = *extended; + g_ime_dlg_has_extended = true; + ext_ptr = &g_ime_dlg_extended; + if (extended->ext_keyboard_filter) { + g_ime_dlg_ext_keyboard_filter = extended->ext_keyboard_filter; + g_ime_dlg_ext_keyboard_filter_user_id = param->user_id; + } + } else { + g_ime_dlg_extended = {}; + g_ime_dlg_has_extended = false; + } + + g_ime_dlg_state = ImeDialogState(&g_ime_dlg_param, ext_ptr); + g_ime_dlg_status = OrbisImeDialogStatus::Running; + if (g_ime_dlg_client) { + g_ime_dlg_client->dialog_state = 4; + } + g_ime_dlg_ui = ImeDialogUi(&g_ime_dlg_state, &g_ime_dlg_status, &g_ime_dlg_result); + + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: successful, status now=Running"); + return Error::OK; +} + +static Error InitDialogCommon(OrbisImeDialogParam* param, OrbisImeParamExtended* extended, + bool internal) { if (param == nullptr) { LOG_ERROR(Lib_ImeDialog, "param is null"); return Error::INVALID_ADDRESS; - } else { + } + if (g_ime_dlg_client != nullptr || g_ime_dlg_status != OrbisImeDialogStatus::None) { + LOG_ERROR(Lib_ImeDialog, "busy (status={})", (u32)g_ime_dlg_status); + return Error::BUSY; + } + + const Error vret = ValidateImeDialogParam(param, extended, internal); + if (vret != Error::OK) { + return vret; + } + + return SetupDialogState(param, extended, internal); +} + +Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended) { + LOG_INFO(Lib_ImeDialog, "Init called, param={}, extended={}", static_cast(param), + static_cast(extended)); + + if (g_ime_dlg_client != nullptr || g_ime_dlg_status != OrbisImeDialogStatus::None) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: busy"); + return Error::BUSY; + } + + if (param != nullptr) { LOG_DEBUG(Lib_ImeDialog, "param->user_id: {}", static_cast(param->user_id)); LOG_DEBUG(Lib_ImeDialog, "param->type: {}", static_cast(param->type)); LOG_DEBUG(Lib_ImeDialog, "param->supported_languages: {:064b}", @@ -355,6 +1075,13 @@ Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExt LOG_DEBUG(Lib_ImeDialog, "param->input_method: {}", static_cast(param->input_method)); LOG_DEBUG(Lib_ImeDialog, "param->filter: {}", (void*)param->filter); LOG_DEBUG(Lib_ImeDialog, "param->option: {:032b}", static_cast(param->option)); + LOG_DEBUG(Lib_ImeDialog, + " opt flags: multiline={}, password={}, ext_kbd={}, fixed_pos={}, over2k={}", + True(param->option & OrbisImeOption::MULTILINE), + 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)); LOG_DEBUG(Lib_ImeDialog, "param->max_text_length: {}", param->max_text_length); LOG_DEBUG(Lib_ImeDialog, "param->input_text_buffer: {}", (void*)param->input_text_buffer); LOG_DEBUG(Lib_ImeDialog, "param->posx: {}", param->posx); @@ -368,56 +1095,6 @@ Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExt LOG_DEBUG(Lib_ImeDialog, "param.title: {}", param->title ? "" : "NULL"); } - if (g_ime_dlg_status != OrbisImeDialogStatus::None) { - LOG_ERROR(Lib_ImeDialog, "busy (status={})", (u32)g_ime_dlg_status); - return Error::BUSY; - } - - if (!magic_enum::enum_contains(param->type)) { - LOG_ERROR(Lib_ImeDialog, "invalid param->type={}", (u32)param->type); - return Error::INVALID_ADDRESS; - } - - // TODO: do correct param->option validation - // TODO: do correct param->supportedLanguages validation - - if (param->posx < 0.0f || - param->posx >= - MAX_X_POSITIONS[False(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES)]) { - LOG_ERROR(Lib_ImeDialog, "Invalid posx: {}", param->posx); - return Error::INVALID_POSX; - } - - if (param->posy < 0.0f || - param->posy >= - MAX_Y_POSITIONS[False(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES)]) { - LOG_ERROR(Lib_ImeDialog, "invalid posy: {}", param->posy); - return Error::INVALID_POSY; - } - - if (!magic_enum::enum_contains(param->horizontal_alignment)) { - LOG_INFO(Lib_ImeDialog, "Invalid param->horizontalAlignment: {}", - (u32)param->horizontal_alignment); - return Error::INVALID_HORIZONTALIGNMENT; - } - - if (!magic_enum::enum_contains(param->vertical_alignment)) { - LOG_INFO(Lib_ImeDialog, "Invalid param->verticalAlignment: {}", - (u32)param->vertical_alignment); - return Error::INVALID_VERTICALALIGNMENT; - } - - if (!IsValidOption(param->option, param->type)) { - LOG_ERROR(Lib_ImeDialog, "Invalid option: {:032b} for type={}", - static_cast(param->option), (u32)param->type); - return Error::INVALID_PARAM; - } - - if (param->input_text_buffer == nullptr) { - LOG_ERROR(Lib_ImeDialog, "Invalid input_text_buffer: null"); - return Error::INVALID_INPUT_TEXT_BUFFER; - } - if (extended) { LOG_DEBUG(Lib_ImeDialog, "extended->option: {:032b}", static_cast(extended->option)); LOG_DEBUG(Lib_ImeDialog, "extended->color_base: {{{},{},{},{}}}", extended->color_base.r, @@ -451,84 +1128,222 @@ Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExt static_cast(extended->disable_device)); LOG_DEBUG(Lib_ImeDialog, "extended->ext_keyboard_mode: {:032b}", static_cast(extended->ext_keyboard_mode)); - LOG_DEBUG(Lib_ImeDialog, "extended->additional_dictionary_path: {}", extended->additional_dictionary_path ? extended->additional_dictionary_path : "NULL"); - LOG_DEBUG(Lib_ImeDialog, "param->filter: {}", (void*)param->filter); + } else { + LOG_DEBUG(Lib_ImeDialog, "extended: NULL"); + } - if (!magic_enum::enum_contains(extended->priority)) { - LOG_INFO(Lib_ImeDialog, "Invalid extended->priority: {}", (u32)extended->priority); - return Error::INVALID_EXTENDED; - } + const u32 sdk = GetCompiledSdkVersion(); + if (!param) { + return Error::INVALID_ADDRESS; + } - // TODO: do correct extended->option validation + const bool legacy_sdk = sdk < 0x1500000; + const Error validate_ret = ValidateImeDialogParam(param, extended, legacy_sdk); + if (validate_ret != Error::OK) { + return validate_ret; + } - if ((extended->ext_keyboard_mode & 0xe3fffffc) != 0) { - LOG_INFO(Lib_ImeDialog, "Invalid extended->extKeyboardMode"); - return Error::INVALID_EXTENDED; - } + OrbisImeDialogParam local_param{}; + OrbisImeDialogParam* param_ptr = param; + if (legacy_sdk) { + local_param = *param; + local_param.option = + static_cast(static_cast(local_param.option) & 0x26ffffffU); + param_ptr = &local_param; + } - if (static_cast(extended->disable_device) & ~kValidOrbisImeDisableDeviceMask) { - LOG_ERROR(Lib_ImeDialog, - "sceImeDialogInit: disable_device has invalid bits set (0x{:X})", - static_cast(extended->disable_device)); - return Error::INVALID_EXTENDED; + u32 user_flags = 0x11; + s64 user_value = 0; + ComputeUserFlags(param->user_id, &user_flags, &user_value); + + g_ime_dlg_resource_id = 0; + g_ime_dlg_client = CreateImeDialogClient(); + if (!g_ime_dlg_client) { + return Error::NO_MEMORY; + } + InitImeDialogClient(g_ime_dlg_client); + + constexpr u32 kImeDialogServiceNotActive = 0x80bc07baU; + constexpr u32 kImeDialogServiceRetry = 0x80bc07b1U; + + const int connect_ret = + ImeDialogClientConnect(g_ime_dlg_client, 2, g_ime_dlg_resource_id, param->user_id, false); + if ((static_cast(connect_ret) & 0xfffffffeU) == kImeDialogServiceNotActive) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: connect returned NOT_ACTIVE (0x{:X})", + static_cast(connect_ret)); + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return Error::NOT_ACTIVE; + } + if (connect_ret == static_cast(Error::INVALID_USER_ID)) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: connect invalid user id"); + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return Error::INVALID_USER_ID; + } + if (connect_ret == static_cast(Error::NOT_ACTIVE) || + static_cast(connect_ret) == kImeDialogServiceRetry) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: connect fallback (0x{:X})", + static_cast(connect_ret)); + const int fallback_ret = ImeDialogClientStartFallback(g_ime_dlg_client, param_ptr, extended, + user_flags, user_value); + if (fallback_ret < 0) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return Error::NOT_ACTIVE; } - } else { - LOG_DEBUG(Lib_ImeDialog, "extended: NULL"); + const Error setup_ret = SetupDialogState(param_ptr, extended, false); + if (setup_ret != Error::OK) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return setup_ret; + } + return static_cast(fallback_ret); + } + if (connect_ret != 0) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: connect failed (0x{:X})", + static_cast(connect_ret)); + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return Error::CONNECTION_FAILED; } - if (param->max_text_length == 0 || param->max_text_length > ORBIS_IME_MAX_TEXT_LENGTH) { - LOG_ERROR(Lib_ImeDialog, "sceImeDialogInit: invalid max_text_length={}", - param->max_text_length); - return Error::INVALID_MAX_TEXT_LENGTH; + int start_ret = ImeDialogClientStart(g_ime_dlg_client, param_ptr, extended, user_flags, + user_value, 0, -1, legacy_sdk); + if (start_ret == 0) { + const Error setup_ret = SetupDialogState(param_ptr, extended, false); + if (setup_ret != Error::OK) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return setup_ret; + } + + if (extended && extended->ext_keyboard_filter) { + const int reg_ret = + RegisterExtKeyboardFilter(param->user_id, extended->ext_keyboard_filter); + if (reg_ret >= 0) { + g_ime_dlg_ext_keyboard_filter_registered = true; + LOG_DEBUG(Lib_ImeDialog, "ext_keyboard_filter registered (user_id={})", + static_cast(g_ime_dlg_ext_keyboard_filter_user_id)); + } else { + g_ime_dlg_ext_keyboard_filter = nullptr; + g_ime_dlg_ext_keyboard_filter_user_id = + Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + } + } + + return Error::OK; } - g_ime_dlg_result = {}; - g_ime_dlg_state = ImeDialogState(param, extended); - g_ime_dlg_status = OrbisImeDialogStatus::Running; - g_ime_dlg_ui = ImeDialogUi(&g_ime_dlg_state, &g_ime_dlg_status, &g_ime_dlg_result); + if (static_cast(start_ret) == kImeDialogServiceRetry) { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: start fallback (0x{:X})", + static_cast(start_ret)); + const int fallback_ret = ImeDialogClientStartFallback(g_ime_dlg_client, param_ptr, extended, + user_flags, user_value); + if (fallback_ret >= 0) { + const Error setup_ret = SetupDialogState(param_ptr, extended, false); + if (setup_ret != Error::OK) { + ImeDialogClientShutdown(g_ime_dlg_client); + DestroyImeDialogClient(); + return setup_ret; + } + return static_cast(fallback_ret); + } + start_ret = static_cast(Error::NOT_ACTIVE); + } else { + LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: start failed (0x{:X})", + static_cast(start_ret)); + ImeDialogClientShutdown(g_ime_dlg_client); + } - LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: successful, status now=Running"); - return Error::OK; + DestroyImeDialogClient(); + return static_cast(start_ret); } -int PS4_SYSV_ABI sceImeDialogInitInternal() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogInitInternal(OrbisImeDialogParam* param, + OrbisImeParamExtended* extended) { + LOG_INFO(Lib_ImeDialog, "InitInternal called, param={}, extended={}", static_cast(param), + static_cast(extended)); + u32 user_flags = 0x11; + s64 user_value = 0; + if (param) { + ComputeUserFlags(param->user_id, &user_flags, &user_value); + } + return InitDialogInternalWithClient(param, extended, user_flags, user_value, 0, 0, 0xffffffff, + true); } -int PS4_SYSV_ABI sceImeDialogInitInternal2() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogInitInternal2(int* param_1, u32* param_2, u32 param_3, u64 param_4) { + LOG_INFO(Lib_ImeDialog, "InitInternal2 called (param={}, extended={}, flags=0x{:X})", + static_cast(param_1), static_cast(param_2), param_3); + auto* param = reinterpret_cast(param_1); + auto* extended = reinterpret_cast(param_2); + return InitDialogInternalWithClient(param, extended, param_3, static_cast(param_4), 0, 0, + 0xffffffff, false); } -int PS4_SYSV_ABI sceImeDialogInitInternal3() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogInitInternal3(int* param_1, u32* param_2, u32 param_3, u64 param_4, + u32 param_5, u32 param_6) { + LOG_INFO(Lib_ImeDialog, + "InitInternal3 called (param={}, extended={}, flags=0x{:X}, resource_id=0x{:X})", + static_cast(param_1), static_cast(param_2), param_3, param_5); + auto* param = reinterpret_cast(param_1); + auto* extended = reinterpret_cast(param_2); + return InitDialogInternalWithClient(param, extended, param_3, static_cast(param_4), + param_5, param_5, param_6, false); } -int PS4_SYSV_ABI sceImeDialogSetPanelPosition() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeDialogSetPanelPosition(s32 posx, s32 posy) { + LOG_INFO(Lib_ImeDialog, "SetPanelPosition called (client_state={}, pos=({}, {}))", + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1, posx, posy); + if (!g_ime_dlg_client) { + return static_cast(Error::DIALOG_NOT_IN_USE); + } + if (g_ime_dlg_status == OrbisImeDialogStatus::Running || + g_ime_dlg_status == OrbisImeDialogStatus::Finished || g_ime_dlg_client->dialog_state == 4 || + g_ime_dlg_client->dialog_state == 5) { + return static_cast(Error::IME_SUSPENDING); + } + g_ime_dlg_param.posx = ClampInternalX(ToInternalCoord(static_cast(posx), g_ime_dlg_param)); + g_ime_dlg_param.posy = ClampInternalY(ToInternalCoord(static_cast(posy), g_ime_dlg_param)); + LOG_DEBUG(Lib_ImeDialog, "SetPanelPosition: pos=({}, {})", g_ime_dlg_param.posx, + g_ime_dlg_param.posy); + return static_cast(Error::OK); } Error PS4_SYSV_ABI sceImeDialogTerm() { - LOG_INFO(Lib_ImeDialog, "called"); - if (g_ime_dlg_status == OrbisImeDialogStatus::None) { + LOG_INFO(Lib_ImeDialog, "Term called (status={}, client_state={})", + static_cast(g_ime_dlg_status), + g_ime_dlg_client ? g_ime_dlg_client->dialog_state : -1); + if (!g_ime_dlg_client) { LOG_INFO(Lib_ImeDialog, "IME dialog not in use"); return Error::DIALOG_NOT_IN_USE; } - if (g_ime_dlg_status == OrbisImeDialogStatus::Running) { - LOG_INFO(Lib_ImeDialog, "IME dialog is still running"); - return Error::DIALOG_NOT_FINISHED; + LOG_DEBUG(Lib_ImeDialog, "Term: status={}, endstatus={}", static_cast(g_ime_dlg_status), + static_cast(g_ime_dlg_result.endstatus)); + CommitDialogResultIfNeeded(); + (void)ImeDialogClientDisconnect(g_ime_dlg_client); + if (g_ime_dlg_ext_keyboard_filter_active) { + NotifyExtKeyboardFilterState(false); } - g_ime_dlg_status = OrbisImeDialogStatus::None; g_ime_dlg_ui = ImeDialogUi(); g_ime_dlg_state = ImeDialogState(); + g_ime_dlg_param = {}; + g_ime_dlg_extended = {}; + g_ime_dlg_has_extended = false; + g_ime_dlg_result_committed = false; + g_ime_dlg_ext_keyboard_filter = nullptr; + g_ime_dlg_ext_keyboard_filter_user_id = + Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + g_ime_dlg_ext_keyboard_filter_registered = false; + g_ime_dlg_ext_keyboard_filter_active = false; + g_ime_dlg_resource_id = 0; + DestroyImeDialogClient(); return Error::OK; } diff --git a/src/core/libraries/ime/ime_dialog.h b/src/core/libraries/ime/ime_dialog.h index 532762ccc61..80fbeab49f6 100644 --- a/src/core/libraries/ime/ime_dialog.h +++ b/src/core/libraries/ime/ime_dialog.h @@ -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); diff --git a/src/core/libraries/ime/ime_dialog_ui.cpp b/src/core/libraries/ime/ime_dialog_ui.cpp index 4a95c60c984..6fc1b639d91 100644 --- a/src/core/libraries/ime/ime_dialog_ui.cpp +++ b/src/core/libraries/ime/ime_dialog_ui.cpp @@ -1,28 +1,119 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include #include +#include #include +#include #include #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(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(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 tmp{}; + ImTextStrFromUtf8(reinterpret_cast(tmp.data()), static_cast(tmp.size()), text, + range_end); + return static_cast(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(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(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(enter_label), static_cast(type)); + LOG_DEBUG(Lib_ImeDialog, + "ImeDialogState: user_id={}, type={}, enter_label={}, multiline={}, numeric={}, " + "max_len={}, text_filter={}, keyboard_filter={}", + static_cast(user_id), static_cast(type), static_cast(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::length(param->title); @@ -64,20 +171,56 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, } } - std::size_t text_len = std::char_traits::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(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(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(current_text.size())); + caret_byte_index = static_cast(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; + } + + if (use_original) { + const std::size_t count = + original_text.empty() ? 0 : static_cast(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(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; +} - return ConvertUTF8ToOrbis(current_text.begin(), current_text.capacity(), text_buffer, - static_cast(max_text_length) + 1); +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(current_text.size())); + if (utf16_len <= static_cast(max_text_length)) { + return false; + } + std::vector utf16(static_cast(max_text_length) + 1, u'\0'); + ImTextStrFromUtf8(reinterpret_cast(utf16.data()), + static_cast(max_text_length) + 1, current_text.begin(), + current_text.begin() + current_text.size()); + size_t len = BoundedUtf16Length(utf16.data(), static_cast(max_text_length)); + std::string out; + out.resize(len * 4 + 1, '\0'); + ImTextStrToUtf8(out.data(), out.size(), reinterpret_cast(utf16.data()), + reinterpret_cast(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( + BoundedUtf16Length(src_text, static_cast(ORBIS_IME_DIALOG_MAX_TEXT_LENGTH))); int ret = Core::ExecuteGuest(text_filter, out_text, &out_text_length, src_text, src_text_length); @@ -144,6 +361,17 @@ bool ImeDialogState::CallTextFilter() { return false; } + const bool changed = NormalizeNewlines() | ClampCurrentTextToMaxLen(); + const int new_len = Utf16CountFromUtf8Range( + current_text.begin(), current_text.begin() + static_cast(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; } @@ -169,7 +397,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(orbis_text), orbis_text_len, utf8_text, nullptr); + const char* utf8_end = utf8_text ? (utf8_text + utf8_text_len) : nullptr; + ImTextStrFromUtf8(reinterpret_cast(orbis_text), orbis_text_len, utf8_text, utf8_end); return true; } @@ -226,6 +455,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 lock{draw_mutex}; @@ -234,21 +475,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(state->panel_req_width) * scale_x, + static_cast(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); + } + + const float panel_w = window_size.x; + const float panel_h = window_size.y; - CentralizeNextWindow(); + 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); @@ -259,80 +546,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(layout.width) + : static_cast(state->panel_req_width); + const float req_h = has_layout ? static_cast(layout.height) + : static_cast(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); - - const char* button_text; - - 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; + 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); + + SetCursorScreenPos(metrics.kb_pos); + + if (!accept_armed) { + if (!IsKeyDown(ImGuiKey_Enter) && !IsKeyDown(ImGuiKey_GamepadFaceDown)) { + accept_armed = true; + LOG_DEBUG(Lib_ImeDialog, "ImeDialog: accept armed"); + } } - - 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) || - (!state->is_multi_line && IsKeyPressed(ImGuiKey_Enter))) { - *status = OrbisImeDialogStatus::Finished; - result->endstatus = OrbisImeDialogEndStatus::Ok; + 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; } - SameLine(0.0f, button_spacing); + Dummy({metrics.kb_size.x, metrics.kb_size.y + metrics.padding_bottom}); - 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(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(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_Multiline); if (first_render) { @@ -340,8 +700,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(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(state->current_text.size())); + if (state->caret_index > new_len) { + state->caret_index = new_len; + } + state->caret_dirty = true; + } + state->CopyTextToOrbisBuffer(false); } } @@ -349,6 +725,31 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ImeDialogUi* ui = static_cast(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); @@ -360,10 +761,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(&data->EventChar); @@ -399,6 +808,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; } diff --git a/src/core/libraries/ime/ime_dialog_ui.h b/src/core/libraries/ime/ime_dialog_ui.h index a0e03a52397..a6178ee0668 100644 --- a/src/core/libraries/ime/ime_dialog_ui.h +++ b/src/core/libraries/ime/ime_dialog_ui.h @@ -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 original_text; std::vector title; std::vector placeholder; @@ -48,8 +64,10 @@ class ImeDialogState final { 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 @@ class ImeDialogUi final : public ImGui::Layer { 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); }; diff --git a/src/core/libraries/ime/ime_kb_layout.cpp b/src/core/libraries/ime/ime_kb_layout.cpp new file mode 100644 index 00000000000..b4377a5f47d --- /dev/null +++ b/src/core/libraries/ime/ime_kb_layout.cpp @@ -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 +#include + +#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(out_res.first) / fb_scale_x; + const float viewport_h = static_cast(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(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(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 row_default = {blank, blank, blank, blank, blank, + blank, blank, blank, blank, blank}; + const std::array row_space = { + blank, blank, blank, {4, nullptr, false}, blank, {2, nullptr, false}, + }; + const std::array 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 diff --git a/src/core/libraries/ime/ime_kb_layout.h b/src/core/libraries/ime/ime_kb_layout.h new file mode 100644 index 00000000000..cf770cd3dc6 --- /dev/null +++ b/src/core/libraries/ime/ime_kb_layout.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#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 diff --git a/src/core/libraries/ime/ime_ui.cpp b/src/core/libraries/ime/ime_ui.cpp index 19b9e2e8b66..744f17a1b44 100644 --- a/src/core/libraries/ime/ime_ui.cpp +++ b/src/core/libraries/ime/ime_ui.cpp @@ -3,6 +3,7 @@ #include #include +#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); - - 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)); + 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; + + u32 panel_req_w = 0; + u32 panel_req_h = 0; + (void)sceImeGetPanelSize(ime_param, &panel_req_w, &panel_req_h); + + 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(); - - 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(); + 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); + + SetWindowFontScale(std::max(viewport.ui_scale, metrics.input_font_scale)); + DrawInputText(metrics); + + 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); + + SetCursorScreenPos(metrics.kb_pos); + + if (!accept_armed) { + if (!IsKeyDown(ImGuiKey_Enter) && !IsKeyDown(ImGuiKey_GamepadFaceDown)) { + accept_armed = true; + } + } + const bool allow_key_accept = accept_armed; + + 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(); } diff --git a/src/core/libraries/ime/ime_ui.h b/src/core/libraries/ime/ime_ui.h index 5eb371bd982..e02534d1c87 100644 --- a/src/core/libraries/ime/ime_ui.h +++ b/src/core/libraries/ime/ime_ui.h @@ -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 @@ class ImeUi : public ImGui::Layer { private: void Free(); - void DrawInputText(); + void DrawInputText(const ImePanelMetrics& metrics); static int InputTextCallback(ImGuiInputTextCallbackData* data); }; -}; // namespace Libraries::Ime \ No newline at end of file +}; // namespace Libraries::Ime diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index ce6446129fa..581b2acab9c 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -47,6 +47,8 @@ struct SwVersionStruct { u32 hex_representation; }; +s32 PS4_SYSV_ABI sceKernelGetSystemSwVersion(SwVersionStruct* ret); + struct AuthInfoData { u64 paid; u64 caps[4];