diff --git a/CMakeLists.txt b/CMakeLists.txt index 952af9148..5e3740b2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -520,12 +520,16 @@ 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 src/core/libraries/ime/ime_dialog.h src/core/libraries/ime/ime_ui.cpp src/core/libraries/ime/ime_ui.h + src/core/libraries/ime/ime_ui_shared.cpp + src/core/libraries/ime/ime_ui_shared.h src/core/libraries/ime/ime.cpp src/core/libraries/ime/ime.h src/core/libraries/ime/ime_error.h diff --git a/src/common/config.h b/src/common/config.h index e8f66ebde..b98b24137 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -163,6 +163,8 @@ std::string getDefaultControllerID(); void setDefaultControllerID(std::string id); bool getBackgroundControllerInput(); void setBackgroundControllerInput(bool enable, bool is_game_specific = false); +bool getLoggingEnabled(); +void setLoggingEnabled(bool enable, bool is_game_specific = false); bool getFsrEnabled(); void setFsrEnabled(bool enable, bool is_game_specific = false); bool getRcasEnabled(); diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index 234e8c2ac..8ff7df1de 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -579,6 +579,8 @@ bool EmulatorSettingsImpl::TransferSettings() { setFromToml(s.motion_controls_enabled, input, "isMotionControlsEnabled"); setFromToml(s.use_unified_input_config, input, "useUnifiedInputConfig"); setFromToml(s.background_controller_input, input, "backgroundControllerInput"); + setFromToml(s.ime_accessibility_enabled, input, "imeAccessibilityEnabled"); + setFromToml(s.ime_url_mail_short_panel, input, "imeUrlMailShortPanel"); setFromToml(s.usb_device_backend, input, "usbDeviceBackend"); } diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index 74fc1b59a..831d411fd 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -291,6 +291,8 @@ struct InputSettings { Setting use_unified_input_config{true}; Setting default_controller_id{""}; Setting background_controller_input{false}; // specific + Setting ime_accessibility_enabled{false}; // specific + Setting ime_url_mail_short_panel{false}; // specific Setting camera_id{-1}; std::vector GetOverrideableFields() const { @@ -303,13 +305,18 @@ struct InputSettings { &InputSettings::motion_controls_enabled), make_override("background_controller_input", &InputSettings::background_controller_input), + make_override("ime_accessibility_enabled", + &InputSettings::ime_accessibility_enabled), + make_override("ime_url_mail_short_panel", + &InputSettings::ime_url_mail_short_panel), make_override("camera_id", &InputSettings::camera_id)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_timeout, usb_device_backend, use_special_pad, special_pad_class, motion_controls_enabled, use_unified_input_config, - default_controller_id, background_controller_input, camera_id) + default_controller_id, background_controller_input, + ime_accessibility_enabled, ime_url_mail_short_panel, camera_id) // ------------------------------- // Audio settings // ------------------------------- @@ -661,6 +668,8 @@ public: SETTING_FORWARD(m_input, UsbDeviceBackend, usb_device_backend) SETTING_FORWARD_BOOL(m_input, MotionControlsEnabled, motion_controls_enabled) SETTING_FORWARD_BOOL(m_input, BackgroundControllerInput, background_controller_input) + SETTING_FORWARD_BOOL(m_input, ImeAccessibilityEnabled, ime_accessibility_enabled) + SETTING_FORWARD_BOOL(m_input, ImeUrlMailShortPanel, ime_url_mail_short_panel) SETTING_FORWARD(m_input, DefaultControllerId, default_controller_id) SETTING_FORWARD_BOOL(m_input, UsingSpecialPad, use_special_pad) SETTING_FORWARD(m_input, SpecialPadClass, special_pad_class) diff --git a/src/core/libraries/ime/ime.cpp b/src/core/libraries/ime/ime.cpp index cc6ab609c..2eb417136 100644 --- a/src/core/libraries/ime/ime.cpp +++ b/src/core/libraries/ime/ime.cpp @@ -1,11 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #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/kernel/process.h" #include "core/libraries/libs.h" #include "core/tls.h" @@ -15,6 +19,152 @@ static std::queue g_ime_events; static std::unique_ptr g_ime_state; static std::unique_ptr g_ime_ui; +namespace { + +u32 GetCompiledSdkVersion() { + s32 ver = 0; + if (Libraries::Kernel::sceKernelGetCompiledSdkVersion(&ver) != ORBIS_OK) { + return 0; + } + return static_cast(ver); +} + +u32 GetImeOptionMask() { + const u32 sdk = GetCompiledSdkVersion(); + u32 mask = static_cast(sdk > 0x14fffff) << 8; + u32 tmp = mask | 0x70ffU; + if (sdk > 0x174ffff) { + tmp = mask | 0x78ffU; + } + mask = tmp & 0x69ffU; + if (sdk > 0x2ffffff) { + mask = tmp; + } + tmp = mask & 0x59ffU; + if (sdk > 0x34fffff) { + tmp = mask; + } + mask = tmp & 0x39ffU; + if (sdk > 0x3ffffff) { + mask = tmp; + } + return mask; +} + +bool HasInvalidImeOption(u32 option) { + return (((GetImeOptionMask() ^ 0xfffffdffU) & option) != 0); +} + +u64 GetImeLanguageMask() { + const u32 sdk = GetCompiledSdkVersion(); + u64 mask = (sdk > 0x1ffffff ? 0x1000000ULL : 0ULL) + 0x3fe1fffffULL; + u64 tmp = mask & 0x3fd1fffffULL; + if (sdk > 0x24fffff) { + tmp = mask; + } + mask = tmp & 0x2031fffffULL; + if (sdk > 0x4ffffff) { + mask = tmp; + } + tmp = mask & 0x1ff1fffffULL; + if (sdk > 0xfffffff) { + tmp = mask; + } + return tmp; +} + +bool IsValidImeExtOption(u32 option) { + const u32 sdk = GetCompiledSdkVersion(); + u32 allow = (sdk < 0x1560000) ? 0x41dfU : 0x4fdfU; + if ((option & 0x4080U) == 0x4000U) { + return false; + } + if (sdk <= 0x5ffffff) { + allow &= 0x0fdfU; + } + return ((~allow & option) == 0); +} + +bool IsValidImeUserId(Libraries::UserService::OrbisUserServiceUserId user_id) { + 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; + } + return user_id >= 0; + } + + 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; + } + return user_id >= 0; +} + +u32 CountUtf16Text(const char16_t* text, u32 max_len) { + if (!text) { + return 0; + } + u32 len = 0; + while (len < max_len && text[len] != u'\0') { + ++len; + } + return len; +} + +bool IsValidImeText(const char16_t* text, u32 length, bool multiline, u32 sdk) { + if (!text) { + return false; + } + const bool allow_new_line = (sdk >= 0x1560000) && multiline; + + for (u32 i = 0; i < length; ++i) { + const char16_t ch = text[i]; + if (!allow_new_line && (ch == u'\n' || ch == u'\r')) { + return false; + } + if ((static_cast(ch) & 0xf800U) == 0xd800U) { + return false; + } + if (ch == u'\0') { + break; + } + } + return true; +} + +bool HasInvalidWorkOverlap(const OrbisImeParam* param) { + if (!param || !param->inputTextBuffer || !param->work) { + return false; + } + + const auto input_addr = reinterpret_cast(param->inputTextBuffer); + const auto work_addr = reinterpret_cast(param->work); + const bool expanded = True(param->option & OrbisImeOption::EXPANDED_PREEDIT_BUFFER); + const u32 scratch_chars = param->maxTextLength + (expanded ? 0x79U : 0x1fU); + + if (input_addr <= work_addr) { + const uintptr_t input_end = + input_addr + static_cast(scratch_chars) * sizeof(char16_t); + if (work_addr < input_end) { + return true; + } + } + + if (work_addr <= input_addr && input_addr <= (work_addr + 0x4fffU)) { + return true; + } + + return false; +} + +} // namespace + class ImeHandler { public: ImeHandler(const OrbisImeKeyboardParam* param) { @@ -83,6 +233,10 @@ public: /* We don't handle any events for ImeKeyboard */ return Error::OK; } + if (!g_ime_state) { + LOG_ERROR(Lib_Ime, "ImeHandler::Update called without active IME state"); + return Error::INTERNAL; + } std::unique_lock lock{g_ime_state->queue_mutex}; @@ -98,18 +252,20 @@ public: void Execute(OrbisImeEventHandler handler, OrbisImeEvent* event, bool use_param_handler) { if (m_ime_mode) { OrbisImeParam param = m_param.ime; - if (use_param_handler) { - param.handler(param.arg, event); - } else { - handler(param.arg, event); + const OrbisImeEventHandler callback = use_param_handler ? param.handler : handler; + if (!callback) { + LOG_ERROR(Lib_Ime, "ImeHandler::Execute called with null IME callback"); + return; } + callback(param.arg, event); } else { OrbisImeKeyboardParam param = m_param.key; - if (use_param_handler) { - param.handler(param.arg, event); - } else { - handler(param.arg, event); + const OrbisImeEventHandler callback = use_param_handler ? param.handler : handler; + if (!callback) { + LOG_ERROR(Lib_Ime, "ImeHandler::Execute called with null keyboard callback"); + return; } + callback(param.arg, event); } } @@ -118,6 +274,10 @@ public: LOG_WARNING(Lib_Ime, "ImeHandler::SetText received null text pointer"); return Error::INVALID_ADDRESS; } + if (!g_ime_state) { + LOG_ERROR(Lib_Ime, "ImeHandler::SetText called without active IME state"); + return Error::INTERNAL; + } g_ime_state->SetText(text, length); return Error::OK; } @@ -127,6 +287,10 @@ public: LOG_WARNING(Lib_Ime, "ImeHandler::SetCaret received null caret pointer"); return Error::INVALID_ADDRESS; } + if (!g_ime_state) { + LOG_ERROR(Lib_Ime, "ImeHandler::SetCaret called without active IME state"); + return Error::INTERNAL; + } g_ime_state->SetCaret(caret->index); return Error::OK; } @@ -135,6 +299,25 @@ public: return m_ime_mode; } + OrbisImeEventHandler GetHandler() const { + return m_ime_mode ? m_param.ime.handler : m_param.key.handler; + } + + u32 GetImeOptionBits() const { + return m_ime_mode ? static_cast(m_param.ime.option) : 0U; + } + + u32 GetImeMaxTextLength() const { + return m_ime_mode ? m_param.ime.maxTextLength : 0U; + } + + u32 GetImeCurrentTextLength() const { + if (!m_ime_mode) { + return 0U; + } + return CountUtf16Text(m_param.ime.inputTextBuffer, m_param.ime.maxTextLength); + } + private: struct ImeParam { OrbisImeKeyboardParam key; @@ -202,7 +385,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,66 +436,43 @@ 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; } Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* height) { - LOG_INFO(Lib_Ime, "called"); - - if (!param) { - LOG_ERROR(Lib_Ime, "Invalid param: NULL"); + if (!param || !width || !height) { return Error::INVALID_ADDRESS; } - - if (!width) { - LOG_ERROR(Lib_Ime, "Invalid *width: NULL"); - return Error::INVALID_ADDRESS; - } - if (!height) { - LOG_ERROR(Lib_Ime, "Invalid *height: NULL"); - 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)); + if (static_cast(param->type) > static_cast(OrbisImeType::Number)) { return Error::INVALID_TYPE; } - LOG_DEBUG(Lib_Ime, "IME panel size: width={}, height={}", *width, *height); + const u32 option = static_cast(param->option); + if (HasInvalidImeOption(option)) { + return Error::INVALID_OPTION; + } + + const u32 sdk = GetCompiledSdkVersion(); + if (param->type == OrbisImeType::Number) { + *width = 0x172U; + *height = (sdk > 0x16fffffU) ? 0x192U : 0x170U; + } else if (param->type == OrbisImeType::BasicLatin || (option & 0xc0000004U) == 4U) { + *width = 0x319U; + *height = (sdk > 0x16fffffU) ? 0x198U : 0x170U; + } else { + *width = 0x319U; + *height = 0x198U; + } + + if ((option & static_cast(OrbisImeOption::USE_OVER_2K_COORDINATES)) != 0) { + *width <<= 1; + *height <<= 1; + } return Error::OK; } @@ -339,7 +500,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 +628,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; } @@ -476,6 +644,11 @@ int PS4_SYSV_ABI sceImeKeyboardUpdate() { Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExtended* extended) { LOG_INFO(Lib_Ime, "called"); + if (g_ime_handler) { + LOG_ERROR(Lib_Ime, "IME handler is already open"); + return Error::BUSY; + } + if (!param) { LOG_ERROR(Lib_Ime, "Invalid param: NULL"); return Error::INVALID_ADDRESS; @@ -540,42 +713,62 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt LOG_DEBUG(Lib_Ime, "extended->ext_keyboard_mode: {}", extended->ext_keyboard_mode); } - if (param->user_id == - Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { // Todo: check valid user IDs + if (!IsValidImeUserId(param->user_id)) { LOG_ERROR(Lib_Ime, "Invalid user_id: {}", static_cast(param->user_id)); return Error::INVALID_USER_ID; } - if (!magic_enum::enum_contains(param->type)) { + if (static_cast(param->type) > static_cast(OrbisImeType::Number)) { LOG_ERROR(Lib_Ime, "Invalid type: {}", static_cast(param->type)); return Error::INVALID_TYPE; } - if (static_cast(param->supported_languages) & ~kValidOrbisImeLanguageMask) { + const u64 lang_mask = GetImeLanguageMask(); + if ((~lang_mask & static_cast(param->supported_languages)) != 0) { LOG_ERROR(Lib_Ime, "Invalid supported_languages\n" "supported_languages: {:064b}\n" "valid_mask: {:064b}", - static_cast(param->supported_languages), kValidOrbisImeLanguageMask); + static_cast(param->supported_languages), lang_mask); return Error::INVALID_SUPPORTED_LANGUAGES; } - if (!magic_enum::enum_contains(param->enter_label)) { + if (static_cast(param->enter_label) > static_cast(OrbisImeEnterLabel::Go)) { LOG_ERROR(Lib_Ime, "Invalid enter_label: {}", static_cast(param->enter_label)); return Error::INVALID_ENTER_LABEL; } - if (!magic_enum::enum_contains(param->input_method)) { + if (param->input_method != OrbisImeInputMethod::Default) { LOG_ERROR(Lib_Ime, "Invalid input_method: {}", static_cast(param->input_method)); return Error::INVALID_INPUT_METHOD; } - if (static_cast(param->option) & ~kValidImeOptionMask) { - LOG_ERROR(Lib_Ime, "option has invalid bits set (0x{:X}), mask=(0x{:X})", - static_cast(param->option), kValidImeOptionMask); + const u32 option = static_cast(param->option); + if (HasInvalidImeOption(option)) { + LOG_ERROR(Lib_Ime, "option has invalid bits set (0x{:X}), mask=(0x{:X})", option, + GetImeOptionMask()); return Error::INVALID_OPTION; } + const bool multiline = True(param->option & OrbisImeOption::MULTILINE); + const bool password = True(param->option & OrbisImeOption::PASSWORD); + if (multiline && password) { + LOG_ERROR(Lib_Ime, "Invalid option combination: MULTILINE + PASSWORD"); + return Error::INVALID_PARAM; + } + if (multiline && param->type != OrbisImeType::Default && + param->type != OrbisImeType::BasicLatin) { + LOG_ERROR(Lib_Ime, "MULTILINE requires type Default or BasicLatin, got {}", + static_cast(param->type)); + return Error::INVALID_PARAM; + } + if (password && param->type != OrbisImeType::BasicLatin && + param->type != OrbisImeType::Number) { + LOG_ERROR(Lib_Ime, "PASSWORD requires type BasicLatin or Number, got {}", + static_cast(param->type)); + return Error::INVALID_PARAM; + } + if (param->maxTextLength == 0 || param->maxTextLength > ORBIS_IME_DIALOG_MAX_TEXT_LENGTH) { LOG_ERROR(Lib_Ime, "Invalid maxTextLength: {}", param->maxTextLength); return Error::INVALID_MAX_TEXT_LENGTH; @@ -586,9 +779,14 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt return Error::INVALID_INPUT_TEXT_BUFFER; } - bool useHighRes = True(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES); - const float maxWidth = useHighRes ? 3840.0f : 1920.0f; - const float maxHeight = useHighRes ? 2160.0f : 1080.0f; + const u32 sdk = GetCompiledSdkVersion(); + float maxWidth = 1920.0f; + float maxHeight = 1080.0f; + if (sdk >= 0x1500000) { + const bool use_high_res = True(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES); + maxWidth = use_high_res ? 3840.0f : 1920.0f; + maxHeight = use_high_res ? 2160.0f : 1080.0f; + } if (param->posx < 0.0f || param->posx >= maxWidth) { LOG_ERROR(Lib_Ime, "Invalid posx: {}, range: 0.0 - {}", param->posx, maxWidth); @@ -599,25 +797,57 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt return Error::INVALID_POSY; } - if (!magic_enum::enum_contains(param->horizontal_alignment)) { + if (static_cast(param->horizontal_alignment) > 2U) { LOG_ERROR(Lib_Ime, "Invalid horizontal_alignment: {}", static_cast(param->horizontal_alignment)); return Error::INVALID_HORIZONTALIGNMENT; } - if (!magic_enum::enum_contains(param->vertical_alignment)) { + if (static_cast(param->vertical_alignment) > 2U) { LOG_ERROR(Lib_Ime, "Invalid vertical_alignment: {}", static_cast(param->vertical_alignment)); return Error::INVALID_VERTICALALIGNMENT; } if (extended) { - u32 ext_option_value = static_cast(extended->option); - if (ext_option_value & ~kValidImeExtOptionMask) { + if (static_cast(extended->priority) > + static_cast(OrbisImePanelPriority::Accent)) { + LOG_ERROR(Lib_Ime, "Invalid extended->priority: {}", + static_cast(extended->priority)); + return Error::INVALID_EXTENDED; + } + + const u32 ext_option_value = static_cast(extended->option); + if (!IsValidImeExtOption(ext_option_value)) { LOG_ERROR(Lib_Ime, "Invalid extended->option\n" "option: {:032b}\n" - "valid_mask: {:032b}", - ext_option_value, kValidImeExtOptionMask); + "sdk: 0x{:X}", + ext_option_value, sdk); + return Error::INVALID_EXTENDED; + } + + if ((extended->ext_keyboard_mode & 0xe3fffffcU) != 0) { + LOG_ERROR(Lib_Ime, "Invalid extended->ext_keyboard_mode: 0x{:X}", + extended->ext_keyboard_mode); + return Error::INVALID_EXTENDED; + } + + for (size_t i = 0; i < sizeof(extended->reserved); ++i) { + if (extended->reserved[i] != 0) { + LOG_ERROR(Lib_Ime, "Invalid extended->reserved: not zeroed"); + return Error::INVALID_EXTENDED; + } + } + + const u32 disable_device = static_cast(extended->disable_device); + if (sdk < 0x1560000) { + if (extended->ext_keyboard_filter != nullptr || disable_device != 0 || + extended->ext_keyboard_mode != 0) { + LOG_ERROR(Lib_Ime, "Invalid extended fields for SDK < 0x1560000"); + return Error::INVALID_EXTENDED; + } + } else if (disable_device > 7U) { + LOG_ERROR(Lib_Ime, "Invalid extended->disable_device: {}", disable_device); return Error::INVALID_EXTENDED; } } @@ -626,16 +856,13 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt LOG_ERROR(Lib_Ime, "Invalid work: NULL"); return Error::INVALID_WORK; } - - // Todo: validate arg - if (false) { - LOG_ERROR(Lib_Ime, "Invalid arg"); - return Error::INVALID_ARG; + if ((reinterpret_cast(param->work) & 0x3U) != 0) { + LOG_ERROR(Lib_Ime, "Invalid work alignment: {:p}", param->work); + return Error::INVALID_WORK; } - // Todo: validate handler - if (false) { - LOG_ERROR(Lib_Ime, "Invalid handler"); + if (!param->handler) { + LOG_ERROR(Lib_Ime, "Invalid handler: NULL"); return Error::INVALID_HANDLER; } @@ -646,11 +873,18 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt } } - if (g_ime_handler) { - LOG_ERROR(Lib_Ime, "IME handler is already open"); - return Error::BUSY; + if (HasInvalidWorkOverlap(param)) { + LOG_ERROR(Lib_Ime, "Invalid overlap between inputTextBuffer and work"); + return Error::INVALID_PARAM; } + if (!IsValidImeText(param->inputTextBuffer, param->maxTextLength, multiline, sdk)) { + LOG_ERROR(Lib_Ime, "Invalid initial inputTextBuffer content"); + return Error::INVALID_TEXT; + } + + std::memset(param->work, 0, 0x5000); + if (extended) { g_ime_handler = std::make_unique(param, extended); } else { @@ -681,7 +915,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; } @@ -692,8 +927,12 @@ Error PS4_SYSV_ABI sceImeSetCaret(const OrbisImeCaret* caret) { if (!g_ime_handler) { return Error::NOT_OPENED; } + const u32 sdk = GetCompiledSdkVersion(); if (!caret) { - return Error::INVALID_ADDRESS; + return (sdk < 0x1500000U) ? Error::INVALID_PARAM : Error::INVALID_ADDRESS; + } + if (caret->index > g_ime_handler->GetImeCurrentTextLength()) { + return Error::INVALID_PARAM; } return g_ime_handler->SetCaret(caret); @@ -711,26 +950,69 @@ Error PS4_SYSV_ABI sceImeSetText(const char16_t* text, u32 length) { return Error::INVALID_ADDRESS; } + const u32 sdk = GetCompiledSdkVersion(); + if (sdk < 0x1560000U && length == 0) { + return Error::INVALID_PARAM; + } + if (length != 0) { + const bool multiline = + (g_ime_handler->GetImeOptionBits() & static_cast(OrbisImeOption::MULTILINE)) != 0; + if (!IsValidImeText(text, length, multiline, sdk)) { + return Error::INVALID_TEXT; + } + } + return g_ime_handler->SetText(text, length); } -int PS4_SYSV_ABI sceImeSetTextGeometry() { - LOG_ERROR(Lib_Ime, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceImeSetTextGeometry(OrbisImeTextAreaMode mode, + const OrbisImeTextGeometry* geometry) { + if (!g_ime_handler) { + return static_cast(Error::NOT_OPENED); + } + if (!geometry) { + return static_cast(Error::INVALID_ADDRESS); + } + + const float x = geometry->x; + const float y = geometry->y; + if (x < 0.0f || x >= 1920.0f || y < 0.0f || y >= 1080.0f) { + return static_cast(Error::INVALID_PARAM); + } + + if (mode != OrbisImeTextAreaMode::Select && mode != OrbisImeTextAreaMode::Preedit) { + return static_cast(Error::INVALID_PARAM); + } + + return static_cast(Error::OK); } Error PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler) { + if (!g_ime_handler && !g_keyboard_handler) { + LOG_ERROR(Lib_Ime, "sceImeUpdate called with no active handler"); + return Error::NOT_OPENED; + } + + const u32 sdk = GetCompiledSdkVersion(); + bool ime_dispatched = false; + if (g_ime_handler) { - g_ime_handler->Update(handler); + if (handler == g_ime_handler->GetHandler()) { + g_ime_handler->Update(handler); + ime_dispatched = true; + } else if (sdk < 0x1500000U) { + ime_dispatched = true; + } else if (!g_keyboard_handler) { + return Error::NOT_OPENED; + } } if (g_keyboard_handler) { g_keyboard_handler->Update(handler); } - if (!g_ime_handler && !g_keyboard_handler) { - LOG_ERROR(Lib_Ime, "sceImeUpdate called with no active handler"); - return Error::OK; + if (!ime_dispatched && !g_keyboard_handler) { + return Error::NOT_OPENED; } return Error::OK; diff --git a/src/core/libraries/ime/ime.h b/src/core/libraries/ime/ime.h index b2b4a51ad..3e0a3d8a3 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 eafb2ce9d..e9ccb85e1 100644 --- a/src/core/libraries/ime/ime_common.h +++ b/src/core/libraries/ime/ime_common.h @@ -121,7 +121,8 @@ enum class OrbisImeExtOption : u32 { DECLARE_ENUM_FLAG_OPERATORS(OrbisImeExtOption); constexpr u32 kValidImeExtOptionMask = static_cast( - OrbisImeExtOption::SET_PRIORITY | OrbisImeExtOption::PRIORITY_FULL_WIDTH | + OrbisImeExtOption::SET_COLOR | OrbisImeExtOption::SET_PRIORITY | + OrbisImeExtOption::PRIORITY_SHIFT | OrbisImeExtOption::PRIORITY_FULL_WIDTH | OrbisImeExtOption::PRIORITY_FIXED_PANEL | OrbisImeExtOption::DISABLE_POINTER | OrbisImeExtOption::ENABLE_ADDITIONAL_DICTIONARY | OrbisImeExtOption::DISABLE_STARTUP_SE | OrbisImeExtOption::DISABLE_LIST_FOR_EXT_KEYBOARD | @@ -375,6 +376,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 +449,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 +483,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 226570bd6..08ce3bb06 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/logging/log.h" +#include "core/emulator_settings.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,897 @@ 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 bool IsValidOption(OrbisImeOption option, OrbisImeType type) { - if (False(~option & (OrbisImeOption::MULTILINE | - OrbisImeOption::NO_AUTO_CAPITALIZATION /* NoAutoCompletion */))) { - return false; +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 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; +} + +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); } - if (True(option & OrbisImeOption::MULTILINE) && type != OrbisImeType::Default && - type != OrbisImeType::BasicLatin) { - return false; + const Error validate_ret = ValidateImeDialogParam(param, extended, true); + if (validate_ret != Error::OK) { + return static_cast(validate_ret); } - if (True(option & OrbisImeOption::NO_AUTO_CAPITALIZATION /* NoAutoCompletion */) && - type != OrbisImeType::Number && type != OrbisImeType::BasicLatin) { - return false; + 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; +} + +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"); + LOG_INFO(Lib_ImeDialog, "Abort rejected: status is not Running ({})", + static_cast(g_ime_dlg_status)); 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(); + 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; + 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; + } + 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; + } } - 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; + if (!extended) { + return sceImeDialogGetPanelSize(param, width, height); + } + + const u32 sdk = GetCompiledSdkVersion(); + if (sdk > 0x16fffff) { + if (!IsValidDialogExtOption(static_cast(extended->option))) { + return Error::INVALID_EXTENDED; + } + } + + 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 = EmulatorSettings.IsImeAccessibilityEnabled(); + } + } + + if (accessibility) { + *width = 0x780; + *height = 0x438; + if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT) { + *width <<= 1; + *height <<= 1; + } + 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; + } + + 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 = !EmulatorSettings.IsImeUrlMailShortPanel(); + 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 { - *width = 1920; - *height = 1080; + *height = 0x1d6; + } + } else { + if (sdk < 0x1700000) { + *height = 0x1d6; + } else { + *height = (sdk < 0x2000000) ? 0x65 : 0x66; } - LOG_DEBUG(Lib_ImeDialog, "Full panel mode: width={}, height={}", *width, *height); - return Error::OK; } - } + } else { + const bool basic_latin_branch = + (type == static_cast(OrbisImeType::BasicLatin)) || ((option & 0xc0000004U) == 4U); - // First get the base panel size from the basic function - Error result = sceImeDialogGetPanelSize(param, width, height); - if (result != Error::OK) { - return result; - } - - // 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; - } - 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; + if (basic_latin_branch) { + *width = 0x319; + if (!multiline) { + if (!hide_keypanel_for_ext) { + *height = (sdk > 0x16fffff) ? 0x210 : 0x1dc; + } else { + *height = (sdk < 0x1700000) ? 0x66 : 0x67; } - - // Check for format characters enabled - if ((extended->ext_keyboard_mode & - static_cast( - OrbisImeInitExtKeyboardMode::ENABLE_FORMAT_CHARACTERS)) != 0) { - *height = *height + 30; + } 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; } } } - - // 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); - } - - // 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 ((param->option & static_cast(0x8)) != OrbisImeOption::DEFAULT) { //? - *width *= 2; - *height *= 2; - LOG_DEBUG(Lib_ImeDialog, "Size mode: width={}, height={}", *width, *height); + if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT) { + *width <<= 1; + *height <<= 1; } - // 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); - } - - // 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); - } + if (log) { + LOG_DEBUG(Lib_ImeDialog, "PanelSizeExt: type={}, option=0x{:X}, sdk=0x{:X}, size={}x{}", + static_cast(param->type), option, sdk, *width, *height); } - - // 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; - } - - LOG_DEBUG(Lib_ImeDialog, "Final panel size: width={}, height={}", *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 +923,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() { - if (g_ime_dlg_status == OrbisImeDialogStatus::Running) { - g_ime_dlg_state.CallTextFilter(); + 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 +1080,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 +1100,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 +1133,227 @@ 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); - - if (!magic_enum::enum_contains(extended->priority)) { - LOG_INFO(Lib_ImeDialog, "Invalid extended->priority: {}", (u32)extended->priority); - return Error::INVALID_EXTENDED; - } - - // TODO: do correct extended->option validation - - if ((extended->ext_keyboard_mode & 0xe3fffffc) != 0) { - LOG_INFO(Lib_ImeDialog, "Invalid extended->extKeyboardMode"); - return Error::INVALID_EXTENDED; - } - - 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; - } } else { LOG_DEBUG(Lib_ImeDialog, "extended: NULL"); } - 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; + const u32 sdk = GetCompiledSdkVersion(); + if (!param) { + return Error::INVALID_ADDRESS; } - 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); + const bool legacy_sdk = sdk < 0x1500000; + const Error validate_ret = ValidateImeDialogParam(param, extended, legacy_sdk); + if (validate_ret != Error::OK) { + return validate_ret; + } - LOG_INFO(Lib_ImeDialog, "sceImeDialogInit: successful, status now=Running"); - return Error::OK; + 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; + } + + 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; + } + 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; + } + + 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; + } + + 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); + } + + 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"); + if (g_ime_dlg_status != OrbisImeDialogStatus::Finished) { + LOG_INFO(Lib_ImeDialog, "Term rejected: status is not Finished ({})", + static_cast(g_ime_dlg_status)); 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 532762ccc..80fbeab49 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 9611e7c49..86bd583a5 100644 --- a/src/core/libraries/ime/ime_dialog_ui.cpp +++ b/src/core/libraries/ime/ime_dialog_ui.cpp @@ -1,28 +1,93 @@ // 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 +#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/libraries/ime/ime_ui_shared.h" +#include "core/libraries/pad/pad.h" +#include "core/memory.h" #include "core/tls.h" #include "imgui/imgui_std.h" +#include "imgui/renderer/imgui_core.h" using namespace ImGui; -static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f}; - namespace Libraries::ImeDialog { +namespace { +constexpr ImU32 kSelectorBorderColor = IM_COL32(248, 248, 248, 255); +constexpr ImU32 kSelectorOverlayColor = IM_COL32(255, 255, 255, 18); +constexpr float kSelectorBorderThickness = 2.0f; +constexpr float kSelectorInnerMargin = 2.0f; +constexpr const char* kSelectorInputId = "##ImeDialogSelectorInput"; +constexpr const char* kSelectorPredictId = "##ImeDialogSelectorPredict"; +constexpr const char* kSelectorCloseId = "##ImeDialogSelectorClose"; + +using Libraries::Ime::BrightenColor; +using Libraries::Ime::ClampInputBufferToUtf16Limit; +using Libraries::Ime::CommitOskPadInputFrame; +using Libraries::Ime::ComputeOskPadInputFrame; +using Libraries::Ime::DrawInactiveCaretOverlay; +using Libraries::Ime::kPanelEdgeWrapHoldDelaySec; +using Libraries::Ime::kRepeatIntentWindowSec; +using Libraries::Ime::OskPadInputFrame; +using Libraries::Ime::OskPadInputState; +using Libraries::Ime::OskVirtualPadInputView; +using Libraries::Ime::ReadVirtualPadSnapshot; +using Libraries::Ime::RejectInputCharByUtf16Limit; +using Libraries::Ime::Utf16CountFromUtf8Range; +using Libraries::Ime::Utf8ByteIndexFromUtf16Index; +using Libraries::Ime::VirtualPadSnapshot; + +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; +} + +} // 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() {} + fixed_position(false), type(OrbisImeType::Default), + supported_languages(static_cast(0)), + enter_label(OrbisImeEnterLabel::Default), text_filter(nullptr), keyboard_filter(nullptr), + max_text_length(ORBIS_IME_DIALOG_MAX_TEXT_LENGTH), text_buffer(nullptr), original_text(), + title(), placeholder(), current_text() {} ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, const OrbisImeParamExtended* extended) { @@ -35,13 +100,35 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, user_id = param->user_id; is_multi_line = True(param->option & OrbisImeOption::MULTILINE); + fixed_position = True(param->option & OrbisImeOption::FIXED_POSITION); + use_over2k = True(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES); is_numeric = param->type == OrbisImeType::Number; type = param->type; + supported_languages = param->supported_languages; enter_label = param->enter_label; text_filter = param->filter; keyboard_filter = extended ? extended->ext_keyboard_filter : nullptr; + ext_option = extended ? extended->option : OrbisImeExtOption::DEFAULT; + disable_device = extended ? extended->disable_device : OrbisImeDisableDevice::DEFAULT; + panel_priority = extended ? extended->priority : OrbisImePanelPriority::Default; + style_config = Libraries::Ime::ResolveImeStyleConfig(extended); 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,19 +151,57 @@ 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), - 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)), + : 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), ext_option(other.ext_option), + disable_device(other.disable_device), panel_priority(other.panel_priority), + style_config(other.style_config), user_id(other.user_id), is_multi_line(other.is_multi_line), + is_numeric(other.is_numeric), fixed_position(other.fixed_position), type(other.type), + supported_languages(other.supported_languages), 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), + 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,15 +210,30 @@ 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; + ext_option = other.ext_option; + disable_device = other.disable_device; + panel_priority = other.panel_priority; + style_config = other.style_config; user_id = other.user_id; is_multi_line = other.is_multi_line; is_numeric = other.is_numeric; + fixed_position = other.fixed_position; type = other.type; + supported_languages = other.supported_languages; 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; + original_text = std::move(other.original_text); title = std::move(other.title); placeholder = std::move(other.placeholder); current_text = other.current_text; @@ -104,13 +244,76 @@ ImeDialogState& ImeDialogState::operator=(ImeDialogState&& other) { return *this; } -bool ImeDialogState::CopyTextToOrbisBuffer() { +bool ImeDialogState::CopyTextToOrbisBuffer(bool use_original) { if (!text_buffer) { + LOG_DEBUG(Lib_ImeDialog, "CopyTextToOrbisBuffer: no text_buffer"); return false; } - return ConvertUTF8ToOrbis(current_text.begin(), current_text.capacity(), text_buffer, - static_cast(max_text_length) + 1); + 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; +} + +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 +324,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 = text_filter(out_text, &out_text_length, src_text, src_text_length); @@ -143,6 +348,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; } @@ -168,7 +384,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; } @@ -178,7 +395,20 @@ ImeDialogUi::ImeDialogUi(ImeDialogState* state, OrbisImeDialogStatus* status, : state(state), status(status), result(result) { if (state && *status == OrbisImeDialogStatus::Running) { + kb_layout_selection = Libraries::Ime::ResolveInitialKbLayoutSelection( + state->ext_option, state->panel_priority); + last_nav_layout_selection = kb_layout_selection; + nav_layout_selection_initialized = true; + kb_alpha_family = + (kb_layout_selection.family == Libraries::Ime::ImeKbLayoutFamily::Specials) + ? Libraries::Ime::ImeKbLayoutFamily::Specials + : Libraries::Ime::ImeKbLayoutFamily::Latin; + Libraries::Ime::InitializeDefaultOskSelectionAnchor( + kb_layout_selection, state->ext_option, pending_keyboard_row, pending_keyboard_col, + last_keyboard_selected_row, last_keyboard_selected_col); AddLayer(this); + ImGui::Core::AcquireGamepadInputCapture(); + gamepad_input_capture_active = true; } } @@ -190,15 +420,103 @@ ImeDialogUi::~ImeDialogUi() { ImeDialogUi::ImeDialogUi(ImeDialogUi&& other) noexcept : state(other.state), status(other.status), result(other.result), - first_render(other.first_render) { + first_render(other.first_render), accept_armed(other.accept_armed), + native_input_active(other.native_input_active), + pointer_navigation_active(other.pointer_navigation_active), + edit_menu_popup(other.edit_menu_popup), menu_activate_armed(other.menu_activate_armed), + l2_shortcut_armed(other.l2_shortcut_armed), request_input_focus(other.request_input_focus), + request_input_select_all(other.request_input_select_all), + text_select_mode(other.text_select_mode), + pending_input_selection_apply(other.pending_input_selection_apply), + prev_virtual_cross_down(other.prev_virtual_cross_down), + prev_virtual_lstick_left_down(other.prev_virtual_lstick_left_down), + prev_virtual_lstick_right_down(other.prev_virtual_lstick_right_down), + prev_virtual_lstick_up_down(other.prev_virtual_lstick_up_down), + prev_virtual_lstick_down_down(other.prev_virtual_lstick_down_down), + left_stick_repeat_dir(other.left_stick_repeat_dir), + left_stick_next_repeat_time(other.left_stick_next_repeat_time), + virtual_cross_next_repeat_time(other.virtual_cross_next_repeat_time), + virtual_triangle_next_repeat_time(other.virtual_triangle_next_repeat_time), + prev_virtual_buttons(other.prev_virtual_buttons), + prev_virtual_square_down(other.prev_virtual_square_down), + prev_virtual_l1_down(other.prev_virtual_l1_down), + prev_virtual_r1_down(other.prev_virtual_r1_down), + prev_virtual_dpad_left_down(other.prev_virtual_dpad_left_down), + prev_virtual_dpad_right_down(other.prev_virtual_dpad_right_down), + prev_virtual_dpad_up_down(other.prev_virtual_dpad_up_down), + prev_virtual_dpad_down_down(other.prev_virtual_dpad_down_down), + virtual_square_next_repeat_time(other.virtual_square_next_repeat_time), + virtual_l1_next_repeat_time(other.virtual_l1_next_repeat_time), + virtual_r1_next_repeat_time(other.virtual_r1_next_repeat_time), + virtual_dpad_left_next_repeat_time(other.virtual_dpad_left_next_repeat_time), + virtual_dpad_right_next_repeat_time(other.virtual_dpad_right_next_repeat_time), + virtual_dpad_up_next_repeat_time(other.virtual_dpad_up_next_repeat_time), + virtual_dpad_down_next_repeat_time(other.virtual_dpad_down_next_repeat_time), + panel_vertical_nav_state(other.panel_vertical_nav_state), + panel_position_initialized(other.panel_position_initialized), + panel_layout_anchor_initialized(other.panel_layout_anchor_initialized), + panel_drag_active(other.panel_drag_active), panel_position(other.panel_position), + panel_layout_anchor(other.panel_layout_anchor), + panel_drag_press_offset(other.panel_drag_press_offset), + input_cursor_utf16(other.input_cursor_utf16), input_cursor_byte(other.input_cursor_byte), + input_selection_start_byte(other.input_selection_start_byte), + input_selection_end_byte(other.input_selection_end_byte), + text_select_anchor_utf16(other.text_select_anchor_utf16), + text_select_focus_utf16(other.text_select_focus_utf16), + top_virtual_col(other.top_virtual_col), panel_selection(other.panel_selection), + pending_keyboard_row(other.pending_keyboard_row), + pending_keyboard_col(other.pending_keyboard_col), + last_keyboard_selected_row(other.last_keyboard_selected_row), + last_keyboard_selected_col(other.last_keyboard_selected_col), + edit_menu_index(other.edit_menu_index), kb_layout_selection(other.kb_layout_selection), + last_nav_layout_selection(other.last_nav_layout_selection), + nav_layout_selection_initialized(other.nav_layout_selection_initialized), + kb_alpha_family(other.kb_alpha_family), + gamepad_input_capture_active(other.gamepad_input_capture_active) { std::scoped_lock lock(draw_mutex, other.draw_mutex); other.state = nullptr; other.status = nullptr; other.result = nullptr; + other.menu_activate_armed = true; + other.l2_shortcut_armed = true; + other.prev_virtual_lstick_left_down = false; + other.prev_virtual_lstick_right_down = false; + other.prev_virtual_lstick_up_down = false; + other.prev_virtual_lstick_down_down = false; + other.left_stick_repeat_dir = 0; + other.left_stick_next_repeat_time = 0.0; + other.virtual_cross_next_repeat_time = 0.0; + other.virtual_triangle_next_repeat_time = 0.0; + other.prev_virtual_buttons = 0; + other.prev_virtual_square_down = false; + other.prev_virtual_l1_down = false; + other.prev_virtual_r1_down = false; + other.prev_virtual_dpad_left_down = false; + other.prev_virtual_dpad_right_down = false; + other.prev_virtual_dpad_up_down = false; + other.prev_virtual_dpad_down_down = false; + other.virtual_square_next_repeat_time = 0.0; + other.virtual_l1_next_repeat_time = 0.0; + other.virtual_r1_next_repeat_time = 0.0; + other.virtual_dpad_left_next_repeat_time = 0.0; + other.virtual_dpad_right_next_repeat_time = 0.0; + other.virtual_dpad_up_next_repeat_time = 0.0; + other.virtual_dpad_down_next_repeat_time = 0.0; + Libraries::Ime::ResetImeEdgeWrapNav(other.panel_vertical_nav_state); + other.nav_layout_selection_initialized = false; + other.panel_layout_anchor_initialized = false; + other.panel_layout_anchor = {}; + other.panel_drag_press_offset = {}; + other.kb_alpha_family = Libraries::Ime::ImeKbLayoutFamily::Latin; + other.gamepad_input_capture_active = false; if (state && *status == OrbisImeDialogStatus::Running) { AddLayer(this); + if (!gamepad_input_capture_active) { + ImGui::Core::AcquireGamepadInputCapture(); + gamepad_input_capture_active = true; + } } } @@ -210,12 +528,107 @@ ImeDialogUi& ImeDialogUi::operator=(ImeDialogUi&& other) { status = other.status; result = other.result; first_render = other.first_render; + accept_armed = other.accept_armed; + native_input_active = other.native_input_active; + pointer_navigation_active = other.pointer_navigation_active; + edit_menu_popup = other.edit_menu_popup; + menu_activate_armed = other.menu_activate_armed; + l2_shortcut_armed = other.l2_shortcut_armed; + request_input_focus = other.request_input_focus; + request_input_select_all = other.request_input_select_all; + text_select_mode = other.text_select_mode; + pending_input_selection_apply = other.pending_input_selection_apply; + prev_virtual_cross_down = other.prev_virtual_cross_down; + prev_virtual_lstick_left_down = other.prev_virtual_lstick_left_down; + prev_virtual_lstick_right_down = other.prev_virtual_lstick_right_down; + prev_virtual_lstick_up_down = other.prev_virtual_lstick_up_down; + prev_virtual_lstick_down_down = other.prev_virtual_lstick_down_down; + left_stick_repeat_dir = other.left_stick_repeat_dir; + left_stick_next_repeat_time = other.left_stick_next_repeat_time; + virtual_cross_next_repeat_time = other.virtual_cross_next_repeat_time; + virtual_triangle_next_repeat_time = other.virtual_triangle_next_repeat_time; + prev_virtual_buttons = other.prev_virtual_buttons; + prev_virtual_square_down = other.prev_virtual_square_down; + prev_virtual_l1_down = other.prev_virtual_l1_down; + prev_virtual_r1_down = other.prev_virtual_r1_down; + prev_virtual_dpad_left_down = other.prev_virtual_dpad_left_down; + prev_virtual_dpad_right_down = other.prev_virtual_dpad_right_down; + prev_virtual_dpad_up_down = other.prev_virtual_dpad_up_down; + prev_virtual_dpad_down_down = other.prev_virtual_dpad_down_down; + virtual_square_next_repeat_time = other.virtual_square_next_repeat_time; + virtual_l1_next_repeat_time = other.virtual_l1_next_repeat_time; + virtual_r1_next_repeat_time = other.virtual_r1_next_repeat_time; + virtual_dpad_left_next_repeat_time = other.virtual_dpad_left_next_repeat_time; + virtual_dpad_right_next_repeat_time = other.virtual_dpad_right_next_repeat_time; + virtual_dpad_up_next_repeat_time = other.virtual_dpad_up_next_repeat_time; + virtual_dpad_down_next_repeat_time = other.virtual_dpad_down_next_repeat_time; + panel_vertical_nav_state = other.panel_vertical_nav_state; + panel_position_initialized = other.panel_position_initialized; + panel_layout_anchor_initialized = other.panel_layout_anchor_initialized; + panel_drag_active = other.panel_drag_active; + panel_position = other.panel_position; + panel_layout_anchor = other.panel_layout_anchor; + panel_drag_press_offset = other.panel_drag_press_offset; + input_cursor_utf16 = other.input_cursor_utf16; + input_cursor_byte = other.input_cursor_byte; + input_selection_start_byte = other.input_selection_start_byte; + input_selection_end_byte = other.input_selection_end_byte; + text_select_anchor_utf16 = other.text_select_anchor_utf16; + text_select_focus_utf16 = other.text_select_focus_utf16; + top_virtual_col = other.top_virtual_col; + panel_selection = other.panel_selection; + pending_keyboard_row = other.pending_keyboard_row; + pending_keyboard_col = other.pending_keyboard_col; + last_keyboard_selected_row = other.last_keyboard_selected_row; + last_keyboard_selected_col = other.last_keyboard_selected_col; + edit_menu_index = other.edit_menu_index; + kb_layout_selection = other.kb_layout_selection; + last_nav_layout_selection = other.last_nav_layout_selection; + nav_layout_selection_initialized = other.nav_layout_selection_initialized; + kb_alpha_family = other.kb_alpha_family; + gamepad_input_capture_active = other.gamepad_input_capture_active; other.state = nullptr; other.status = nullptr; other.result = nullptr; + other.menu_activate_armed = true; + other.l2_shortcut_armed = true; + other.prev_virtual_lstick_left_down = false; + other.prev_virtual_lstick_right_down = false; + other.prev_virtual_lstick_up_down = false; + other.prev_virtual_lstick_down_down = false; + other.left_stick_repeat_dir = 0; + other.left_stick_next_repeat_time = 0.0; + other.virtual_cross_next_repeat_time = 0.0; + other.virtual_triangle_next_repeat_time = 0.0; + other.prev_virtual_buttons = 0; + other.prev_virtual_square_down = false; + other.prev_virtual_l1_down = false; + other.prev_virtual_r1_down = false; + other.prev_virtual_dpad_left_down = false; + other.prev_virtual_dpad_right_down = false; + other.prev_virtual_dpad_up_down = false; + other.prev_virtual_dpad_down_down = false; + other.virtual_square_next_repeat_time = 0.0; + other.virtual_l1_next_repeat_time = 0.0; + other.virtual_r1_next_repeat_time = 0.0; + other.virtual_dpad_left_next_repeat_time = 0.0; + other.virtual_dpad_right_next_repeat_time = 0.0; + other.virtual_dpad_up_next_repeat_time = 0.0; + other.virtual_dpad_down_next_repeat_time = 0.0; + Libraries::Ime::ResetImeEdgeWrapNav(other.panel_vertical_nav_state); + other.nav_layout_selection_initialized = false; + other.panel_layout_anchor_initialized = false; + other.panel_layout_anchor = {}; + other.panel_drag_press_offset = {}; + other.kb_alpha_family = Libraries::Ime::ImeKbLayoutFamily::Latin; + other.gamepad_input_capture_active = false; if (state && *status == OrbisImeDialogStatus::Running) { AddLayer(this); + if (!gamepad_input_capture_active) { + ImGui::Core::AcquireGamepadInputCapture(); + gamepad_input_capture_active = true; + } } return *this; @@ -223,6 +636,22 @@ ImeDialogUi& ImeDialogUi::operator=(ImeDialogUi&& other) { void ImeDialogUi::Free() { RemoveLayer(this); + if (gamepad_input_capture_active) { + ImGui::Core::ReleaseGamepadInputCapture(); + gamepad_input_capture_active = false; + } +} + +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() { @@ -233,136 +662,1475 @@ void ImeDialogUi::Draw() { } if (!status || *status != OrbisImeDialogStatus::Running) { + Free(); return; } const auto& ctx = *GetCurrentContext(); const auto& io = ctx.IO; + const bool imgui_typing_mode_active = + native_input_active || request_input_focus || pending_input_selection_apply; + const bool ps4_typing_mode_active = !imgui_typing_mode_active; + const VirtualPadSnapshot virtual_pad = + ReadVirtualPadSnapshot(state->user_id, io.DeltaTime, !ps4_typing_mode_active); + + 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; ImVec2 window_size; - - if (state->is_multi_line) { - window_size = {500.0f, 300.0f}; + 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); } - CentralizeNextWindow(); + const float panel_w = window_size.x; + const float panel_h = window_size.y; + + float base_x = 0.0f; + float base_y = 0.0f; + if (has_layout) { + base_x = viewport_offset.x + layout.posx * scale_x; + base_y = viewport_offset.y + layout.posy * scale_y; + if (layout.horizontal_alignment == OrbisImeHorizontalAlignment::Center) { + base_x -= window_size.x * 0.5f; + } else if (layout.horizontal_alignment == OrbisImeHorizontalAlignment::Right) { + base_x -= window_size.x; + } + if (layout.vertical_alignment == OrbisImeVerticalAlignment::Center) { + base_y -= window_size.y * 0.5f; + } else if (layout.vertical_alignment == OrbisImeVerticalAlignment::Bottom) { + base_y -= window_size.y; + } + } else { + base_x = viewport_offset.x + (viewport_size.x - window_size.x) * 0.5f; + base_y = viewport_offset.y + (viewport_size.y - window_size.y) * 0.5f; + } + const float min_x = viewport_offset.x; + const float max_x = viewport_offset.x + std::max(0.0f, viewport_size.x - window_size.x); + const float min_y = viewport_offset.y; + const float max_y = viewport_offset.y + std::max(0.0f, viewport_size.y - window_size.y); + base_x = std::clamp(base_x, min_x, max_x); + base_y = std::clamp(base_y, min_y, max_y); + const ImVec2 layout_anchor{base_x, base_y}; + if (!panel_layout_anchor_initialized) { + panel_layout_anchor = layout_anchor; + panel_layout_anchor_initialized = true; + } + const ImVec2 layout_anchor_delta{layout_anchor.x - panel_layout_anchor.x, + layout_anchor.y - panel_layout_anchor.y}; + + if (!panel_position_initialized) { + panel_position = layout_anchor; + panel_position_initialized = true; + } + if (state->fixed_position) { + panel_position = layout_anchor; + panel_drag_active = false; + } else { + panel_position.x += layout_anchor_delta.x; + panel_position.y += layout_anchor_delta.y; + if (!ps4_typing_mode_active) { + panel_drag_active = false; + } else { + const ImVec2 mouse_pos = io.MousePos; + const bool mouse_over_panel = mouse_pos.x >= panel_position.x && + mouse_pos.x <= (panel_position.x + window_size.x) && + mouse_pos.y >= panel_position.y && + mouse_pos.y <= (panel_position.y + window_size.y); + if (!panel_drag_active && IsMouseClicked(ImGuiMouseButton_Left, false) && + mouse_over_panel) { + panel_drag_active = true; + // Preserve the initial grab offset so cursor stays on the pressed panel point. + panel_drag_press_offset = {mouse_pos.x - panel_position.x, + mouse_pos.y - panel_position.y}; + } + if (panel_drag_active) { + if (IsMouseDown(ImGuiMouseButton_Left)) { + panel_position.x = mouse_pos.x - panel_drag_press_offset.x; + panel_position.y = mouse_pos.y - panel_drag_press_offset.y; + } else { + panel_drag_active = false; + } + } + const ImVec2 right_stick_delta = virtual_pad.panel_delta; + panel_position.x += right_stick_delta.x; + panel_position.y += right_stick_delta.y; + } + } + panel_layout_anchor = layout_anchor; + panel_position.x = std::clamp(panel_position.x, min_x, max_x); + panel_position.y = std::clamp(panel_position.y, min_y, max_y); + SetNextWindowPos(panel_position); SetNextWindowSize(window_size); + SetNextWindowDockID(0, ImGuiCond_Always); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { SetNextWindowFocus(); } - if (Begin("IME Dialog##ImeDialog", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { + const auto lock_window_scroll = []() { + SetScrollX(0.0f); + SetScrollY(0.0f); + ImGuiWindow* window = GetCurrentWindow(); + if (!window) { + return; + } + window->Scroll = ImVec2(0.0f, 0.0f); + const float max_f = std::numeric_limits::max(); + window->ScrollTarget = ImVec2(max_f, max_f); + }; + + ImGuiWindowFlags window_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + window_flags |= ImGuiWindowFlags_NoNavInputs; + if (Begin("IME Dialog##ImeDialog", nullptr, window_flags)) { + lock_window_scroll(); + KeepNavHighlight(); 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); + + const bool controller_shortcuts_disabled = + True(state->disable_device & OrbisImeDisableDevice::CONTROLLER); + const bool allow_osk_shortcuts = ps4_typing_mode_active && !controller_shortcuts_disabled; + OskPadInputState panel_pad_state{ + prev_virtual_buttons, + prev_virtual_cross_down, + prev_virtual_lstick_left_down, + prev_virtual_lstick_right_down, + prev_virtual_lstick_up_down, + prev_virtual_lstick_down_down, + left_stick_repeat_dir, + left_stick_next_repeat_time, + virtual_cross_next_repeat_time, + prev_virtual_dpad_left_down, + prev_virtual_dpad_right_down, + prev_virtual_dpad_up_down, + prev_virtual_dpad_down_down, + virtual_dpad_left_next_repeat_time, + virtual_dpad_right_next_repeat_time, + virtual_dpad_up_next_repeat_time, + virtual_dpad_down_next_repeat_time, + }; + const OskPadInputFrame panel_input = ComputeOskPadInputFrame( + virtual_pad, allow_osk_shortcuts, first_render, panel_pad_state); + + const OskVirtualPadInputView virtual_pad_input(panel_input, io); + const bool cross_down = panel_input.cross_down; + const bool panel_activate_pressed_raw = panel_input.panel_activate_pressed_raw; + const bool panel_activate_repeat_raw = panel_input.panel_activate_repeat_raw; + const bool nav_left = panel_input.nav_left; + const bool nav_right = panel_input.nav_right; + const bool nav_up = panel_input.nav_up; + const bool nav_down = panel_input.nav_down; + const bool nav_left_repeat = panel_input.nav_left_repeat; + const bool nav_right_repeat = panel_input.nav_right_repeat; + const bool nav_up_repeat = panel_input.nav_up_repeat; + const bool nav_down_repeat = panel_input.nav_down_repeat; + const bool virtual_control_input = panel_input.virtual_control_input; + const bool osk_control_input = panel_input.osk_control_input; + + const bool raw_osk_control_input = panel_input.raw_osk_control_input; + if (native_input_active && raw_osk_control_input && !request_input_focus && + !pending_input_selection_apply) { + native_input_active = false; + request_input_focus = false; + pending_input_selection_apply = false; + ImGui::ClearActiveID(); + } + const double nav_now = ImGui::GetTime(); + const bool cancel_shortcut_pressed = + allow_osk_shortcuts && + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::Circle); + const ImVec2 mouse_delta = ImGui::GetIO().MouseDelta; + const bool pointer_input = IsMouseClicked(ImGuiMouseButton_Left, false) || + IsMouseClicked(ImGuiMouseButton_Right, false) || + IsMouseClicked(ImGuiMouseButton_Middle, false) || + (mouse_delta.x != 0.0f || mouse_delta.y != 0.0f); + if (pointer_input) { + pointer_navigation_active = true; + } + if (osk_control_input) { + pointer_navigation_active = false; + } + if (native_input_active && virtual_control_input && !request_input_focus && + !pending_input_selection_apply) { + native_input_active = false; + ImGui::ClearActiveID(); + } + if (!accept_armed) { + if (!cross_down) { + accept_armed = true; + LOG_DEBUG(Lib_ImeDialog, "ImeDialog: accept armed"); + } + } + const bool panel_activate_pressed = + allow_osk_shortcuts && accept_armed && + (panel_activate_pressed_raw || panel_activate_repeat_raw); + + using SelectionIndex = Libraries::Ime::ImeSelectionGridIndex; + const auto& selected_kb_layout = Libraries::Ime::GetImeKeyboardLayout(kb_layout_selection); + const bool nav_layout_changed = + nav_layout_selection_initialized && + (last_nav_layout_selection.family != kb_layout_selection.family || + last_nav_layout_selection.case_state != kb_layout_selection.case_state || + last_nav_layout_selection.page != kb_layout_selection.page); + if (nav_layout_changed) { + Libraries::Ime::ResetImeEdgeWrapNav(panel_vertical_nav_state); + } + last_nav_layout_selection = kb_layout_selection; + nav_layout_selection_initialized = true; + const int keyboard_min_col = 0; + const int keyboard_min_row = 0; + const int keyboard_cols = std::max(1, static_cast(selected_kb_layout.cols)); + const int keyboard_rows = std::max(1, static_cast(selected_kb_layout.rows)); + const int keyboard_max_col = keyboard_cols - 1; + const int keyboard_max_row = keyboard_rows - 1; + const auto keyboard_vertical_wraps_from = [&](int from_row, int from_col, int step_row) { + return Libraries::Ime::DoesImeKeyboardNavigationWrap(selected_kb_layout, from_row, + from_col, step_row, 0); + }; + + const auto& top_layout_cfg = + Libraries::Ime::GetImeTopPanelLayoutConfig(kb_layout_selection); + const int top_panel_row = + SelectionIndex::PanelTopRowFromConfig(static_cast(top_layout_cfg.row)); + const int top_panel_rows = + SelectionIndex::PanelTopRowsFromConfig(static_cast(top_layout_cfg.row_span)); + const int keyboard_panel_min_row = + SelectionIndex::PanelKeyboardMinRowForTopPanel(top_panel_row, top_panel_rows); + const int keyboard_panel_max_row = SelectionIndex::PanelKeyboardMaxRowForKeyboardRows( + keyboard_rows, top_panel_row, top_panel_rows); + const int top_col_min = 0; + const int top_cols_cfg = std::max(1, static_cast(top_layout_cfg.cols)); + const int top_col_max = top_col_min + top_cols_cfg - 1; + + const auto top_to_keyboard_col = [&](int top_col) { + const int clamped_top = std::clamp(top_col, top_col_min, top_col_max); + if (keyboard_cols <= 1 || top_cols_cfg <= 1) { + return keyboard_min_col; + } + const int top_offset = clamped_top - top_col_min; + return keyboard_min_col + (top_offset * (keyboard_cols - 1)) / (top_cols_cfg - 1); + }; + const auto keyboard_to_top_col = [&](int keyboard_col) { + const int clamped_kb = std::clamp(keyboard_col, keyboard_min_col, keyboard_max_col); + if (top_cols_cfg <= 1 || keyboard_cols <= 1) { + return top_col_min; + } + const int kb_offset = clamped_kb - keyboard_min_col; + return top_col_min + (kb_offset * (top_cols_cfg - 1)) / (keyboard_cols - 1); + }; + + struct TopNavElement { + PanelSelectionTarget target = PanelSelectionTarget::Prediction; + int min_col = 0; + int max_col = 0; + }; + + std::vector top_col_to_element_index(static_cast(top_cols_cfg), -1); + std::vector top_elements; + top_elements.reserve(static_cast(top_cols_cfg)); + + const auto append_top_element = [&](Libraries::Ime::ImeTopPanelElementId id, int min_col, + int max_col) { + PanelSelectionTarget target = PanelSelectionTarget::Prediction; + switch (id) { + case Libraries::Ime::ImeTopPanelElementId::Prediction: + target = PanelSelectionTarget::Prediction; + break; + case Libraries::Ime::ImeTopPanelElementId::Close: + target = PanelSelectionTarget::Close; + break; + default: + return; + } + + if (static_cast(top_elements.size()) >= top_cols_cfg) { + return; + } + if (max_col < min_col) { + return; + } + + const int clamped_min = std::clamp(min_col, top_col_min, top_col_max); + const int clamped_max = std::clamp(max_col, top_col_min, top_col_max); + if (clamped_max < clamped_min) { + return; + } + + top_elements.push_back({target, clamped_min, clamped_max}); + const int element_index = static_cast(top_elements.size()) - 1; + for (int col = clamped_min; col <= clamped_max; ++col) { + top_col_to_element_index[static_cast(col - top_col_min)] = + element_index; + } + }; + + for (std::size_t i = 0; i < top_layout_cfg.element_count; ++i) { + const auto& spec = top_layout_cfg.elements[i]; + const int min_col = std::clamp(static_cast(spec.col), top_col_min, top_col_max); + const int span = std::max(1, static_cast(spec.col_span)); + const int max_col = std::clamp(min_col + span - 1, top_col_min, top_col_max); + append_top_element(spec.id, min_col, max_col); + } + if (top_elements.empty()) { + append_top_element(Libraries::Ime::ImeTopPanelElementId::Prediction, top_col_min, + top_col_max); + } + + const auto element_index_for_col = [&](int col) { + if (col < top_col_min || col > top_col_max) { + return -1; + } + return top_col_to_element_index[static_cast(col - top_col_min)]; + }; + const auto element_index_for_target = [&](PanelSelectionTarget target) { + for (int i = 0; i < static_cast(top_elements.size()); ++i) { + if (top_elements[static_cast(i)].target == target) { + return i; + } + } + return -1; + }; + const auto set_top_selection = [&](PanelSelectionTarget target, int preferred_col) { + panel_selection = target; + const int target_idx = element_index_for_target(target); + if (target_idx < 0) { + top_virtual_col = std::clamp(preferred_col, top_col_min, top_col_max); + return; + } + const auto& element = top_elements[static_cast(target_idx)]; + top_virtual_col = std::clamp(preferred_col, element.min_col, element.max_col); + }; + const auto top_col_for_selection = [&](PanelSelectionTarget target) { + const int target_idx = element_index_for_target(target); + const int clamped_col = std::clamp(top_virtual_col, top_col_min, top_col_max); + if (target_idx < 0) { + return clamped_col; + } + const auto& element = top_elements[static_cast(target_idx)]; + return std::clamp(clamped_col, element.min_col, element.max_col); + }; + const bool menu_modal = (edit_menu_popup != EditMenuPopup::None); + const int keyboard_row_before_panel_nav_raw = + (pending_keyboard_row >= keyboard_min_row && pending_keyboard_row <= keyboard_max_row) + ? pending_keyboard_row + : last_keyboard_selected_row; + const int keyboard_col_before_panel_nav_raw = + (pending_keyboard_col >= keyboard_min_col && pending_keyboard_col <= keyboard_max_col) + ? pending_keyboard_col + : last_keyboard_selected_col; + const int keyboard_row_before_panel_nav = + std::clamp(keyboard_row_before_panel_nav_raw, keyboard_min_row, keyboard_max_row); + const int keyboard_col_before_panel_nav = + std::clamp(keyboard_col_before_panel_nav_raw, keyboard_min_col, keyboard_max_col); + bool entered_top_from_keyboard = false; + const auto move_keyboard_edge_to_top = [&](int wrap_dir_y, int keyboard_col) { + const int top_col = keyboard_to_top_col(keyboard_col); + const int element_idx = element_index_for_col(top_col); + if (element_idx >= 0) { + const auto& element = top_elements[static_cast(element_idx)]; + set_top_selection(element.target, top_col); + } else if (!top_elements.empty()) { + const auto& first_element = top_elements[0]; + set_top_selection(first_element.target, first_element.min_col); + } + Libraries::Ime::CommitImeEdgeWrapStep(panel_vertical_nav_state, wrap_dir_y, 0, nav_now); + entered_top_from_keyboard = true; + }; + if (!menu_modal && !text_select_mode && !pointer_navigation_active && + panel_selection == PanelSelectionTarget::Keyboard) { + const int wrap_dir_y = + (nav_up && keyboard_vertical_wraps_from(keyboard_row_before_panel_nav, + keyboard_col_before_panel_nav, -1)) + ? -1 + : ((nav_down && keyboard_vertical_wraps_from(keyboard_row_before_panel_nav, + keyboard_col_before_panel_nav, 1)) + ? 1 + : 0); + if (wrap_dir_y != 0) { + const bool hold_before_wrap_to_top = wrap_dir_y > 0; + const bool wrap_repeat = nav_down_repeat; + if (!hold_before_wrap_to_top || + !Libraries::Ime::ShouldDelayImeEdgeWrap( + panel_vertical_nav_state, wrap_dir_y, 0, wrap_repeat, true, nav_now, + kPanelEdgeWrapHoldDelaySec, kRepeatIntentWindowSec)) { + move_keyboard_edge_to_top(wrap_dir_y, keyboard_col_before_panel_nav); + } + } + } + + 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)); } + bool input_hovered = false; if (state->is_multi_line) { - DrawMultiLineInputText(); + input_hovered = DrawMultiLineInputText(metrics, pointer_navigation_active); } else { - DrawInputText(); + input_hovered = DrawInputText(metrics, pointer_navigation_active); + } + const bool input_selected = + pointer_navigation_active && (input_hovered || native_input_active); + const bool input_clicked = pointer_navigation_active && input_hovered && + IsMouseClicked(ImGuiMouseButton_Left, false); + static std::unordered_map + s_selector_fade_states; + const auto draw_selector = [&](const char* selector_id, ImVec2 pos, ImVec2 size, + bool selected, bool pulse_triggered) { + auto& fade_state = s_selector_fade_states[ImGui::GetID(selector_id)]; + const double now = ImGui::GetTime(); + if (selected && pulse_triggered) { + Libraries::Ime::TriggerSelectorPressPulse(fade_state, now); + } + const float selector_corner_radius = + std::max(0.0f, metrics.corner_radius - kSelectorInnerMargin); + Libraries::Ime::UpdateSelectorFadeState(fade_state, pos, size, kSelectorInnerMargin, + selector_corner_radius, selected, now); + const float press_pulse_expand = + selected ? Libraries::Ime::ComputePressPulseExpand( + fade_state.press_pulse_started_at, now, + Libraries::Ime::kSelectorPressPulseDurationSec, + kSelectorBorderThickness * + Libraries::Ime::kSelectorPressPulseExpandBorderFactor) + : 0.0f; + Libraries::Ime::DrawSelectorFadeState( + fade_state, GetWindowDrawList(), kSelectorOverlayColor, kSelectorBorderColor, + kSelectorBorderThickness, Libraries::Ime::kSelectorFadeOutDurationSec, now, + press_pulse_expand); + }; + draw_selector(kSelectorInputId, metrics.input_pos_screen, metrics.input_size, + input_selected, + input_clicked || (panel_selection == PanelSelectionTarget::Input && + panel_activate_pressed_raw)); + + auto* draw = GetWindowDrawList(); + const ImU32 pane_bg = Libraries::Ime::ImeColorToImU32(state->style_config.color_base); + 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); + SetCursorScreenPos(metrics.predict_pos); + PushID("##ImeDialogPredict"); + PushItemFlag(ImGuiItemFlags_NoNav, true); + InvisibleButton("##ImeDialogPredict", metrics.predict_size); + PopItemFlag(); + const bool prediction_clicked = + IsMouseClicked(ImGuiMouseButton_Left, false) && IsItemHovered(); + const int prediction_element_idx = + element_index_for_target(PanelSelectionTarget::Prediction); + if (pointer_navigation_active && prediction_clicked && prediction_element_idx >= 0) { + const auto& prediction_element = + top_elements[static_cast(prediction_element_idx)]; + set_top_selection(PanelSelectionTarget::Prediction, + SelectionIndex::GridColumnFromX( + io.MousePos.x, metrics.predict_pos.x, metrics.predict_size.x, + prediction_element.min_col, prediction_element.max_col)); + } + PopID(); + draw_selector(kSelectorPredictId, metrics.predict_pos, metrics.predict_size, + panel_selection == PanelSelectionTarget::Prediction, + prediction_clicked || (panel_selection == PanelSelectionTarget::Prediction && + panel_activate_pressed_raw)); + const ImU32 close_button_bg = + Libraries::Ime::ImeColorToImU32(state->style_config.color_button_function); + PushStyleColor(ImGuiCol_Button, BrightenColor(close_button_bg, 0.0f)); + PushStyleColor(ImGuiCol_ButtonHovered, BrightenColor(close_button_bg, 0.08f)); + PushStyleColor(ImGuiCol_ButtonActive, BrightenColor(close_button_bg, 0.16f)); + SetCursorScreenPos(metrics.close_pos); + PushItemFlag(ImGuiItemFlags_NoNav, true); + bool cancel_pressed = + Button("##ImeDialogClose", {metrics.close_size.x, metrics.close_size.y}); + PopItemFlag(); + constexpr const char* kCloseLabel = "\xE2\x9C\x95"; + const ImVec2 close_label_size = CalcTextSize(kCloseLabel, nullptr, true); + const float close_pad_y = std::max(1.0f, metrics.close_size.y * 0.04f); + const ImVec2 close_label_pos{metrics.close_pos.x + + (metrics.close_size.x - close_label_size.x) * 0.5f, + metrics.close_pos.y + close_pad_y}; + draw->AddText(close_label_pos, + Libraries::Ime::ImeColorToImU32(state->style_config.color_text), kCloseLabel); + cancel_pressed = cancel_pressed || cancel_shortcut_pressed; + const bool close_clicked = IsMouseClicked(ImGuiMouseButton_Left, false) && IsItemHovered(); + const int close_element_idx = element_index_for_target(PanelSelectionTarget::Close); + if (pointer_navigation_active && close_clicked && close_element_idx >= 0) { + const auto& close_element = top_elements[static_cast(close_element_idx)]; + set_top_selection(PanelSelectionTarget::Close, close_element.min_col); + } + PopStyleColor(3); + draw_selector(kSelectorCloseId, metrics.close_pos, metrics.close_size, + panel_selection == PanelSelectionTarget::Close, + close_clicked || (panel_selection == PanelSelectionTarget::Close && + panel_activate_pressed_raw)); + if (!cancel_pressed && panel_selection == PanelSelectionTarget::Close && + panel_activate_pressed) { + cancel_pressed = true; } - SetCursorPosY(GetCursorPosY() + 10.0f); + SetCursorScreenPos(metrics.kb_pos); - const char* button_text; + bool entered_keyboard_from_top = false; + const auto move_top_navigation = [&](int dir_x, int dir_y) { + const PanelSelectionTarget current = panel_selection; + int col = top_col_for_selection(current); + int origin_element_idx = element_index_for_col(col); + if (origin_element_idx < 0) { + origin_element_idx = element_index_for_target(current); + } - switch (state->enter_label) { - case OrbisImeEnterLabel::Go: - button_text = "Go##ImeDialogOK"; + if (dir_x != 0) { + if (origin_element_idx >= 0) { + const int span = top_col_max - top_col_min + 1; + bool crossed_wrap = false; + for (int step = 1; step <= span; ++step) { + const int next_col_raw = col + dir_x * step; + crossed_wrap = crossed_wrap || next_col_raw < top_col_min || + next_col_raw > top_col_max; + const int next_col = + top_col_min + (col - top_col_min + dir_x * step + span) % span; + const int next_element_idx = element_index_for_col(next_col); + if (next_element_idx < 0 || next_element_idx == origin_element_idx) { + continue; + } + const bool repeat_hint = dir_x < 0 ? nav_left_repeat : nav_right_repeat; + if (Libraries::Ime::ShouldDelayImeEdgeWrap( + panel_vertical_nav_state, 0, dir_x, repeat_hint, crossed_wrap, + nav_now, kPanelEdgeWrapHoldDelaySec, kRepeatIntentWindowSec)) { + return; + } + const auto& next_element = + top_elements[static_cast(next_element_idx)]; + set_top_selection(next_element.target, next_col); + Libraries::Ime::CommitImeEdgeWrapStep(panel_vertical_nav_state, 0, dir_x, + nav_now); + return; + } + } + } + + if (origin_element_idx >= 0) { + if (dir_y > 0) { + pending_keyboard_row = SelectionIndex::PanelToKeyboardRow( + keyboard_panel_min_row, keyboard_rows, top_panel_row, top_panel_rows); + pending_keyboard_col = top_to_keyboard_col(col); + panel_selection = PanelSelectionTarget::Keyboard; + entered_keyboard_from_top = true; + Libraries::Ime::CommitImeEdgeWrapStep(panel_vertical_nav_state, dir_y, 0, + nav_now); + return; + } + if (dir_y < 0) { + if (Libraries::Ime::ShouldDelayImeEdgeWrap( + panel_vertical_nav_state, dir_y, 0, nav_up_repeat, true, nav_now, + kPanelEdgeWrapHoldDelaySec, kRepeatIntentWindowSec)) { + return; + } + pending_keyboard_row = SelectionIndex::PanelToKeyboardRow( + keyboard_panel_max_row, keyboard_rows, top_panel_row, top_panel_rows); + pending_keyboard_col = top_to_keyboard_col(col); + panel_selection = PanelSelectionTarget::Keyboard; + entered_keyboard_from_top = true; + Libraries::Ime::CommitImeEdgeWrapStep(panel_vertical_nav_state, dir_y, 0, + nav_now); + return; + } + } + }; + if (!menu_modal && !text_select_mode && !pointer_navigation_active && + !entered_top_from_keyboard && panel_selection != PanelSelectionTarget::Keyboard) { + if (nav_left) { + move_top_navigation(-1, 0); + } else if (nav_right) { + move_top_navigation(1, 0); + } else if (nav_up) { + move_top_navigation(0, -1); + } else if (nav_down) { + move_top_navigation(0, 1); + } + } + + bool accept_pressed = false; + const auto text_length_utf16 = [&]() { + const char* text = state->current_text.begin(); + const int byte_len = static_cast(state->current_text.size()); + return Utf16CountFromUtf8Range(text, text ? (text + byte_len) : nullptr); + }; + const auto sync_text_buffers = [&]() { + state->input_changed = true; + (void)state->CopyTextToOrbisBuffer(false); + }; + const auto apply_selection_state = [&]() { + const int len = text_length_utf16(); + const int caret = std::clamp( + (text_select_focus_utf16 >= 0) + ? text_select_focus_utf16 + : ((state->caret_index >= 0) ? state->caret_index : input_cursor_utf16), + 0, len); + const int anchor = + text_select_mode ? std::clamp(text_select_anchor_utf16, 0, len) : caret; + const int focus = + text_select_mode ? std::clamp(text_select_focus_utf16, 0, len) : caret; + const char* text = state->current_text.begin(); + const int anchor_byte = Utf8ByteIndexFromUtf16Index(text ? text : "", anchor); + const int focus_byte = Utf8ByteIndexFromUtf16Index(text ? text : "", focus); + input_cursor_utf16 = focus; + input_cursor_byte = focus_byte; + input_selection_start_byte = std::min(anchor_byte, focus_byte); + input_selection_end_byte = std::max(anchor_byte, focus_byte); + state->caret_index = focus; + state->caret_byte_index = focus_byte; + state->caret_dirty = native_input_active; + if (native_input_active) { + pending_input_selection_apply = true; + request_input_focus = true; + } else { + pending_input_selection_apply = false; + request_input_focus = false; + } + }; + const auto apply_text_edit = [&](int start_utf16, int end_utf16, + const char* insert_utf8) -> bool { + const std::string current = state->current_text.to_string(); + const char* current_text = current.c_str(); + const int len_utf16 = Utf16CountFromUtf8Range( + current_text, current_text + static_cast(current.size())); + const int start = std::clamp(start_utf16, 0, len_utf16); + const int end = std::clamp(end_utf16, start, len_utf16); + const int removed_utf16 = end - start; + const int available_utf16 = + static_cast(state->max_text_length) - (len_utf16 - removed_utf16); + + std::string insert_clamped{}; + if (insert_utf8 && insert_utf8[0] != '\0' && available_utf16 > 0) { + const char* p = insert_utf8; + int used_utf16 = 0; + while (*p && used_utf16 < available_utf16) { + unsigned int codepoint = 0; + const int step = ImTextCharFromUtf8(&codepoint, p, nullptr); + if (step <= 0) { + break; + } + const int units = (codepoint > 0xFFFF) ? 2 : 1; + if (used_utf16 + units > available_utf16) { + break; + } + insert_clamped.append(p, static_cast(step)); + p += step; + used_utf16 += units; + } + } + + if (removed_utf16 == 0 && insert_clamped.empty()) { + return false; + } + + const int start_byte = Utf8ByteIndexFromUtf16Index(current_text, start); + const int end_byte = Utf8ByteIndexFromUtf16Index(current_text, end); + std::string updated{}; + updated.reserve(current.size() - static_cast(end_byte - start_byte) + + insert_clamped.size()); + updated.append(current, 0, static_cast(start_byte)); + updated.append(insert_clamped); + updated.append(current, static_cast(end_byte), std::string::npos); + + state->current_text.FromString(updated); + if (state->is_multi_line) { + (void)state->NormalizeNewlines(); + } + (void)state->ClampCurrentTextToMaxLen(); + sync_text_buffers(); + + const int inserted_utf16 = Utf16CountFromUtf8Range( + insert_clamped.c_str(), + insert_clamped.c_str() + static_cast(insert_clamped.size())); + const int new_caret_utf16 = std::clamp(start + inserted_utf16, 0, text_length_utf16()); + text_select_mode = false; + text_select_anchor_utf16 = new_caret_utf16; + text_select_focus_utf16 = new_caret_utf16; + apply_selection_state(); + return true; + }; + const auto selection_utf16_range = [&]() -> std::pair { + const int len = text_length_utf16(); + if (!text_select_mode) { + const int caret = std::clamp(state->caret_index, 0, len); + return {caret, caret}; + } + const int anchor = std::clamp(text_select_anchor_utf16, 0, len); + const int focus = std::clamp(text_select_focus_utf16, 0, len); + return {std::min(anchor, focus), std::max(anchor, focus)}; + }; + const auto insert_text_at_caret = [&](const char* suffix) { + const auto [sel_start, sel_end] = selection_utf16_range(); + return apply_text_edit(sel_start, sel_end, suffix); + }; + const auto backspace_at_caret = [&]() { + const auto [sel_start, sel_end] = selection_utf16_range(); + if (sel_end > sel_start) { + return apply_text_edit(sel_start, sel_end, ""); + } + if (sel_end <= 0) { + return false; + } + const std::string current = state->current_text.to_string(); + const char* text = current.c_str(); + const int caret_byte = Utf8ByteIndexFromUtf16Index(text, sel_end); + int prev_byte = caret_byte; + do { + --prev_byte; + } while (prev_byte > 0 && + (static_cast(text[prev_byte]) & 0xC0u) == 0x80u); + const int prev_utf16 = Utf16CountFromUtf8Range(text, text + prev_byte); + return apply_text_edit(prev_utf16, sel_end, ""); + }; + const auto clear_all_text = [&]() { + const int len = text_length_utf16(); + if (len <= 0) { + return false; + } + // Matches libSceIme backend all-delete behavior (sceImeBackendAllDeleteConvertString): + // clear the whole editable string in one operation. + return apply_text_edit(0, len, ""); + }; + + 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.cols = keyboard_cols; + kb_layout.rows = keyboard_rows; + const int layout_rows = std::max(1, kb_layout.rows); + kb_layout.fixed_bottom_rows = SelectionIndex::ResolveFunctionRows( + layout_rows, static_cast(selected_kb_layout.function_rows)); + if (kb_layout.fixed_bottom_rows > 0) { + kb_layout.bottom_row_h = std::max(8.0f, metrics.key_h); + const int typing_rows = std::max(1, layout_rows - kb_layout.fixed_bottom_rows); + const float typing_area_h = + kb_layout.size.y - kb_layout.key_gap_y * static_cast(layout_rows - 1) - + kb_layout.bottom_row_h * static_cast(kb_layout.fixed_bottom_rows); + const float computed_typing_key_h = typing_area_h / static_cast(typing_rows); + kb_layout.key_h = std::max(8.0f, computed_typing_key_h); + } else { + kb_layout.fixed_bottom_rows = 0; + kb_layout.bottom_row_h = 0.0f; + const float computed_key_h = + (kb_layout.size.y - kb_layout.key_gap_y * static_cast(layout_rows - 1)) / + static_cast(layout_rows); + kb_layout.key_h = std::max(8.0f, computed_key_h); + } + kb_layout.corner_radius = metrics.corner_radius; + + Libraries::Ime::ImeKbDrawParams kb_params{}; + kb_params.selection = kb_layout_selection; + kb_params.layout_model = &selected_kb_layout; + kb_params.supported_languages = state->supported_languages; + kb_params.enter_label = state->enter_label; + kb_params.show_selection_highlight = (panel_selection == PanelSelectionTarget::Keyboard); + kb_params.allow_nav_input = allow_osk_shortcuts && !menu_modal && !text_select_mode && + (panel_selection == PanelSelectionTarget::Keyboard) && + !entered_keyboard_from_top; + kb_params.use_imgui_lstick_nav = false; + kb_params.allow_activate_input = allow_osk_shortcuts && accept_armed && !menu_modal && + !text_select_mode && + (panel_selection == PanelSelectionTarget::Keyboard); + ApplyOskPanelNavToKeyboardParams(kb_params, allow_osk_shortcuts, panel_input); + kb_params.external_activate_pressed = panel_activate_pressed; + kb_params.external_activate_repeat = + allow_osk_shortcuts && accept_armed && panel_activate_repeat_raw; + kb_params.reset_nav_state = nav_layout_changed; + kb_params.requested_selected_row = pending_keyboard_row; + kb_params.requested_selected_col = pending_keyboard_col; + Libraries::Ime::ApplyImeStyleToKeyboardDrawParams(state->style_config, kb_params); + pending_keyboard_row = -1; + pending_keyboard_col = -1; + + 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.selected_row >= 0 && kb_state.selected_col >= 0) { + last_keyboard_selected_row = kb_state.selected_row; + last_keyboard_selected_col = kb_state.selected_col; + } + if (pointer_navigation_active && kb_state.clicked) { + panel_selection = PanelSelectionTarget::Keyboard; + } + + const auto consume_temporary_uppercase = [&](bool typed_character) { + if (typed_character && + kb_layout_selection.case_state == Libraries::Ime::ImeKbCaseState::Upper) { + kb_layout_selection.case_state = Libraries::Ime::ImeKbCaseState::Lower; + } + }; + const auto has_clipboard_text = [&]() { + const char* text = ImGui::GetClipboardText(); + return text && text[0] != '\0'; + }; + const auto get_selection_byte_range = [&]() { + const int text_len = static_cast(state->current_text.size()); + const int sel_start = std::clamp(input_selection_start_byte, 0, text_len); + const int sel_end = std::clamp(input_selection_end_byte, 0, text_len); + return std::pair{std::min(sel_start, sel_end), std::max(sel_start, sel_end)}; + }; + const auto copy_selected_text = [&]() { + const std::string current = state->current_text.to_string(); + const auto [sel_start, sel_end] = get_selection_byte_range(); + if (sel_end > sel_start) { + const std::string selection = + current.substr(static_cast(sel_start), + static_cast(sel_end - sel_start)); + ImGui::SetClipboardText(selection.c_str()); + } else { + ImGui::SetClipboardText(current.c_str()); + } + }; + const auto collapse_selection_to_caret = [&](int caret_utf16) { + const int len = text_length_utf16(); + const int clamped_caret = std::clamp(caret_utf16, 0, len); + text_select_mode = false; + text_select_anchor_utf16 = clamped_caret; + text_select_focus_utf16 = clamped_caret; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto move_text_caret = [&](int delta_utf16, bool preserve_selection) { + const int len = text_length_utf16(); + int base = text_select_mode ? text_select_focus_utf16 : input_cursor_utf16; + if (base < 0) { + base = input_cursor_utf16; + } + base = std::clamp(base, 0, len); + const int next = std::clamp(base + delta_utf16, 0, len); + if (preserve_selection && text_select_mode) { + text_select_focus_utf16 = next; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + return next != base; + } + collapse_selection_to_caret(next); + return next != base; + }; + const auto move_text_caret_to_boundary = [&](bool to_end, bool preserve_selection) { + const int len = text_length_utf16(); + int base = text_select_mode ? text_select_focus_utf16 : input_cursor_utf16; + if (base < 0) { + base = input_cursor_utf16; + } + base = std::clamp(base, 0, len); + const int next = to_end ? len : 0; + if (preserve_selection && text_select_mode) { + text_select_focus_utf16 = next; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + return next != base; + } + collapse_selection_to_caret(next); + return next != base; + }; + const auto begin_text_selection_from_caret = [&]() { + text_select_mode = true; + const int len = text_length_utf16(); + const int caret = std::clamp(input_cursor_utf16, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto select_all_text = [&]() { + text_select_mode = true; + const int len = text_length_utf16(); + text_select_anchor_utf16 = 0; + text_select_focus_utf16 = len; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto open_main_menu = [&]() { + Libraries::Ime::OpenOskMainEditMenu(edit_menu_popup, edit_menu_index, + menu_activate_armed); + }; + const auto open_actions_menu = [&]() { + Libraries::Ime::OpenOskActionsEditMenu(edit_menu_popup, edit_menu_index, + menu_activate_armed); + }; + const auto apply_main_menu_action = [&](int action_index) { + switch (action_index) { + case 0: // Select + begin_text_selection_from_caret(); + edit_menu_popup = EditMenuPopup::None; + break; + case 1: // Select All + select_all_text(); + open_actions_menu(); + break; + case 2: // Paste + if (has_clipboard_text()) { + (void)insert_text_at_caret(ImGui::GetClipboardText()); + } + edit_menu_popup = EditMenuPopup::None; + break; + default: + break; + } + }; + const auto apply_actions_menu_action = [&](int action_index) { + switch (action_index) { + case 0: // Copy + copy_selected_text(); + collapse_selection_to_caret(text_select_focus_utf16 >= 0 ? text_select_focus_utf16 + : input_cursor_utf16); + edit_menu_popup = EditMenuPopup::None; + break; + case 1: // Paste + if (has_clipboard_text()) { + (void)insert_text_at_caret(ImGui::GetClipboardText()); + } + edit_menu_popup = EditMenuPopup::None; + break; + default: + break; + } + }; + + bool opened_menu_this_frame = false; + Libraries::Ime::OskShortcutRepeatState shortcut_repeat_state{ + prev_virtual_square_down, + prev_virtual_l1_down, + prev_virtual_r1_down, + l2_shortcut_armed, + virtual_square_next_repeat_time, + virtual_l1_next_repeat_time, + virtual_r1_next_repeat_time, + virtual_triangle_next_repeat_time}; + const Libraries::Ime::OskShortcutActionResult shortcut_action = + Libraries::Ime::EvaluateOskShortcutAction( + allow_osk_shortcuts, menu_modal, + kb_state.pressed_action == Libraries::Ime::ImeKbKeyAction::None, panel_input, + virtual_pad_input, prev_virtual_buttons, kb_layout_selection.family, + shortcut_repeat_state); + bool keyboard_action_from_hotkey = false; + if (shortcut_action.clear_all) { + (void)clear_all_text(); + } else if (shortcut_action.action != Libraries::Ime::ImeKbKeyAction::None) { + kb_state.pressed_action = shortcut_action.action; + keyboard_action_from_hotkey = true; + } + switch (kb_state.pressed_action) { + case Libraries::Ime::ImeKbKeyAction::Character: + consume_temporary_uppercase(insert_text_at_caret(kb_state.pressed_label)); break; - case OrbisImeEnterLabel::Search: - button_text = "Search##ImeDialogOK"; + case Libraries::Ime::ImeKbKeyAction::Shift: + Libraries::Ime::CycleKeyboardCaseState(kb_layout_selection); break; - case OrbisImeEnterLabel::Send: - button_text = "Send##ImeDialogOK"; + case Libraries::Ime::ImeKbKeyAction::SymbolsMode: + Libraries::Ime::ToggleKeyboardFamilyMode(kb_layout_selection, kb_alpha_family, + Libraries::Ime::ImeKbLayoutFamily::Symbols); + if (!keyboard_action_from_hotkey && + Libraries::Ime::FocusKeyboardActionKeySelection( + kb_layout_selection, Libraries::Ime::ImeKbKeyAction::SymbolsMode, + pending_keyboard_row, pending_keyboard_col)) { + last_keyboard_selected_row = pending_keyboard_row; + last_keyboard_selected_col = pending_keyboard_col; + panel_selection = PanelSelectionTarget::Keyboard; + } + break; + case Libraries::Ime::ImeKbKeyAction::SpecialsMode: + Libraries::Ime::ToggleKeyboardFamilyMode(kb_layout_selection, kb_alpha_family, + Libraries::Ime::ImeKbLayoutFamily::Specials); + if (!keyboard_action_from_hotkey && + Libraries::Ime::FocusKeyboardActionKeySelection( + kb_layout_selection, Libraries::Ime::ImeKbKeyAction::SpecialsMode, + pending_keyboard_row, pending_keyboard_col)) { + last_keyboard_selected_row = pending_keyboard_row; + last_keyboard_selected_col = pending_keyboard_col; + panel_selection = PanelSelectionTarget::Keyboard; + } + break; + case Libraries::Ime::ImeKbKeyAction::ArrowLeft: + (void)move_text_caret(-1, text_select_mode); + break; + case Libraries::Ime::ImeKbKeyAction::ArrowRight: + (void)move_text_caret(1, text_select_mode); + break; + case Libraries::Ime::ImeKbKeyAction::ArrowUp: + if (!state->is_multi_line) { + // Single-line OSK behavior: up jumps caret to the beginning. + (void)move_text_caret_to_boundary(false, text_select_mode); + } + break; + case Libraries::Ime::ImeKbKeyAction::ArrowDown: + if (!state->is_multi_line) { + // Single-line OSK behavior: down jumps caret to the end. + (void)move_text_caret_to_boundary(true, text_select_mode); + } + break; + case Libraries::Ime::ImeKbKeyAction::PagePrev: + Libraries::Ime::FlipKeyboardModePage(kb_layout_selection, -1); + break; + case Libraries::Ime::ImeKbKeyAction::PageNext: + Libraries::Ime::FlipKeyboardModePage(kb_layout_selection, 1); + break; + case Libraries::Ime::ImeKbKeyAction::Space: + (void)insert_text_at_caret(" "); + break; + case Libraries::Ime::ImeKbKeyAction::Backspace: + (void)backspace_at_caret(); + break; + case Libraries::Ime::ImeKbKeyAction::NewLine: + if (state->is_multi_line) { + (void)insert_text_at_caret("\n"); + } else { + accept_pressed = true; + } + break; + case Libraries::Ime::ImeKbKeyAction::Menu: + if (edit_menu_popup == EditMenuPopup::None) { + open_main_menu(); + opened_menu_this_frame = true; + } else { + edit_menu_popup = EditMenuPopup::None; + menu_activate_armed = true; + } + break; + case Libraries::Ime::ImeKbKeyAction::Settings: + pointer_navigation_active = !pointer_navigation_active; + if (!pointer_navigation_active) { + if (native_input_active) { + native_input_active = false; + ImGui::ClearActiveID(); + } + panel_selection = PanelSelectionTarget::Keyboard; + } + break; + case Libraries::Ime::ImeKbKeyAction::Done: + accept_pressed = true; break; - case OrbisImeEnterLabel::Default: default: - button_text = "OK##ImeDialogOK"; break; } - - 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; + if (kb_state.done_pressed) { + accept_pressed = true; } - SameLine(0.0f, button_spacing); - - if (Button("Cancel##ImeDialogCancel", BUTTON_SIZE)) { - *status = OrbisImeDialogStatus::Finished; - result->endstatus = OrbisImeDialogEndStatus::UserCanceled; + if (text_select_mode && edit_menu_popup == EditMenuPopup::None && + !pointer_navigation_active) { + int delta = 0; + if (nav_left || nav_up) { + delta = -1; + } else if (nav_right || nav_down) { + delta = 1; + } + if (delta != 0) { + const int len = text_length_utf16(); + if (text_select_anchor_utf16 < 0 || text_select_focus_utf16 < 0) { + const int caret = std::clamp(input_cursor_utf16, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + } + text_select_focus_utf16 = std::clamp(text_select_focus_utf16 + delta, 0, len); + apply_selection_state(); + } else if (panel_activate_pressed) { + open_actions_menu(); + opened_menu_this_frame = true; + } } + + if (Libraries::Ime::CloseOskEditMenuOnCancel(edit_menu_popup, cancel_pressed, + menu_activate_armed)) { + // Popup closed. + } else if (text_select_mode && cancel_pressed) { + cancel_pressed = false; + text_select_mode = false; + const int len = text_length_utf16(); + const int caret = std::clamp(text_select_focus_utf16, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + apply_selection_state(); + } + + if (edit_menu_popup != EditMenuPopup::None) { + const bool clipboard_ready = has_clipboard_text(); + const auto previous_popup = edit_menu_popup; + (void)Libraries::Ime::DrawAndHandleOskEditMenuPopup( + edit_menu_popup, edit_menu_index, metrics, draw, pointer_navigation_active, nav_up, + nav_down, cross_down, panel_activate_pressed, opened_menu_this_frame, + menu_activate_armed, clipboard_ready, 2000, "##ImeDialogEditMenuItem", true, + [&](const EditMenuPopup source_popup, const int action_index) { + if (source_popup == EditMenuPopup::Main) { + apply_main_menu_action(action_index); + } else { + apply_actions_menu_action(action_index); + } + }); + if (edit_menu_popup != EditMenuPopup::None && edit_menu_popup != previous_popup) { + opened_menu_this_frame = true; + } + } + + Dummy({metrics.kb_size.x, metrics.kb_size.y + metrics.padding_bottom}); + + 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"); + } + CommitOskPadInputFrame(panel_input, panel_pad_state); + lock_window_scroll(); + SetWindowFontScale(1.0f); } End(); first_render = false; } -void ImeDialogUi::DrawInputText() { - ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f}; - SetCursorPosX(20.0f); - if (first_render) { +bool ImeDialogUi::DrawInputText(const Libraries::Ime::ImePanelMetrics& metrics, + bool pointer_selection_enabled) { + const ImVec2 input_size = metrics.input_size; + SetCursorPos(metrics.input_pos_local); + if (request_input_focus) { SetKeyboardFocusHere(); + request_input_focus = false; } - 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)) { - state->input_changed = true; - } -} -void ImeDialogUi::DrawMultiLineInputText() { - ImVec2 input_size = {GetWindowWidth() - 40.0f, 200.0f}; - SetCursorPosX(20.0f); - ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackCharFilter | - static_cast(ImGuiInputTextFlags_Multiline); - if (first_render) { + if (state->caret_dirty && !text_select_mode) { + const char* text = state->current_text.begin(); + const int len_utf16 = + Utf16CountFromUtf8Range(text, text + static_cast(state->current_text.size())); + int caret_utf16 = state->caret_index; + if (caret_utf16 < 0) { + caret_utf16 = 0; + } else if (caret_utf16 > len_utf16) { + caret_utf16 = len_utf16; + } + const int caret_byte = Utf8ByteIndexFromUtf16Index(text, caret_utf16); + state->caret_index = caret_utf16; + state->caret_byte_index = caret_byte; + input_cursor_utf16 = caret_utf16; + input_cursor_byte = caret_byte; + input_selection_start_byte = caret_byte; + input_selection_end_byte = caret_byte; + text_select_anchor_utf16 = caret_utf16; + text_select_focus_utf16 = caret_utf16; + if (native_input_active) { + pending_input_selection_apply = true; + request_input_focus = true; + } else { + pending_input_selection_apply = false; + request_input_focus = false; + state->caret_dirty = false; + } + } + + const ImVec2 rect_min = metrics.input_pos_screen; + const ImVec2 rect_max{rect_min.x + input_size.x, rect_min.y + input_size.y}; + const bool clicked_input = IsMouseClicked(ImGuiMouseButton_Left, false) && + IsMouseHoveringRect(rect_min, rect_max, false); + if (clicked_input) { + native_input_active = true; SetKeyboardFocusHere(); } + + ImGuiInputTextFlags flags = + ImGuiInputTextFlags_CallbackCharFilter | ImGuiInputTextFlags_CallbackAlways; + if (!native_input_active) { + flags |= ImGuiInputTextFlags_ReadOnly; + } + const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data(); + PushStyleColor(ImGuiCol_FrameBg, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_text_field)); + PushStyleColor(ImGuiCol_FrameBgHovered, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_preedit)); + PushStyleColor(ImGuiCol_FrameBgActive, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_preedit)); + PushStyleColor(ImGuiCol_Text, Libraries::Ime::ImeColorToImVec4(state->style_config.color_text)); + PushItemFlag(ImGuiItemFlags_NoNav, true); if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(), state->max_text_length * 4 + 1, input_size, flags, 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); } + PopItemFlag(); + PopStyleColor(4); + const ImRect frame_rect = {GetItemRectMin(), GetItemRectMax()}; + if (!IsItemActive()) { + DrawInactiveCaretOverlay(frame_rect, state->current_text.begin(), input_cursor_byte, + input_selection_start_byte, input_selection_end_byte, false); + } + const bool hovered = IsItemHovered(); + if (IsMouseClicked(ImGuiMouseButton_Left, false) && !hovered && native_input_active) { + native_input_active = false; + } + return pointer_selection_enabled && (hovered || IsItemActive() || clicked_input); +} + +bool ImeDialogUi::DrawMultiLineInputText(const Libraries::Ime::ImePanelMetrics& metrics, + bool pointer_selection_enabled) { + const ImVec2 input_size = metrics.input_size; + SetCursorPos(metrics.input_pos_local); + if (request_input_focus) { + SetKeyboardFocusHere(); + request_input_focus = false; + } + if (state->caret_dirty && !text_select_mode) { + const char* text = state->current_text.begin(); + const int len_utf16 = + Utf16CountFromUtf8Range(text, text + static_cast(state->current_text.size())); + int caret_utf16 = state->caret_index; + if (caret_utf16 < 0) { + caret_utf16 = 0; + } else if (caret_utf16 > len_utf16) { + caret_utf16 = len_utf16; + } + const int caret_byte = Utf8ByteIndexFromUtf16Index(text, caret_utf16); + state->caret_index = caret_utf16; + state->caret_byte_index = caret_byte; + input_cursor_utf16 = caret_utf16; + input_cursor_byte = caret_byte; + input_selection_start_byte = caret_byte; + input_selection_end_byte = caret_byte; + text_select_anchor_utf16 = caret_utf16; + text_select_focus_utf16 = caret_utf16; + if (native_input_active) { + pending_input_selection_apply = true; + request_input_focus = true; + } else { + pending_input_selection_apply = false; + request_input_focus = false; + state->caret_dirty = false; + } + } + ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackCharFilter | + static_cast(ImGuiInputTextFlags_Multiline) | + ImGuiInputTextFlags_CallbackAlways; + + const ImVec2 rect_min = metrics.input_pos_screen; + const ImVec2 rect_max{rect_min.x + input_size.x, rect_min.y + input_size.y}; + const bool clicked_input = IsMouseClicked(ImGuiMouseButton_Left, false) && + IsMouseHoveringRect(rect_min, rect_max, false); + if (clicked_input) { + native_input_active = true; + SetKeyboardFocusHere(); + } + if (!native_input_active) { + flags |= ImGuiInputTextFlags_ReadOnly; + } + + const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data(); + PushStyleColor(ImGuiCol_FrameBg, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_text_field)); + PushStyleColor(ImGuiCol_FrameBgHovered, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_preedit)); + PushStyleColor(ImGuiCol_FrameBgActive, + Libraries::Ime::ImeColorToImVec4(state->style_config.color_preedit)); + PushStyleColor(ImGuiCol_Text, Libraries::Ime::ImeColorToImVec4(state->style_config.color_text)); + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(), + state->max_text_length * 4 + 1, input_size, flags, 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); + } + PopItemFlag(); + PopStyleColor(4); + const ImRect frame_rect = {GetItemRectMin(), GetItemRectMax()}; + if (!IsItemActive()) { + DrawInactiveCaretOverlay(frame_rect, state->current_text.begin(), input_cursor_byte, + input_selection_start_byte, input_selection_end_byte, true); + } + const bool hovered = IsItemHovered(); + if (IsMouseClicked(ImGuiMouseButton_Left, false) && !hovered && native_input_active) { + native_input_active = false; + } + return pointer_selection_enabled && (hovered || IsItemActive() || clicked_input); } int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ImeDialogUi* ui = static_cast(data->UserData); ASSERT(ui); + if (!ui->state) { + return 1; + } + + if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) { + int buf_len = std::max(0, data->BufTextLen); + if (ui->request_input_select_all) { + data->SelectAll(); + ui->request_input_select_all = false; + } + if (ui->pending_input_selection_apply) { + const int len_chars = Utf16CountFromUtf8Range(data->Buf, data->Buf + buf_len); + int anchor = ui->text_select_anchor_utf16; + int focus = ui->text_select_focus_utf16; + if (anchor < 0 || focus < 0) { + const int caret_utf16 = + Utf16CountFromUtf8Range(data->Buf, data->Buf + data->CursorPos); + anchor = caret_utf16; + focus = caret_utf16; + } + anchor = std::clamp(anchor, 0, len_chars); + focus = std::clamp(focus, 0, len_chars); + const int anchor_byte = Utf8ByteIndexFromUtf16Index(data->Buf, anchor); + const int focus_byte = Utf8ByteIndexFromUtf16Index(data->Buf, focus); + data->CursorPos = focus_byte; + data->SelectionStart = std::min(anchor_byte, focus_byte); + data->SelectionEnd = std::max(anchor_byte, focus_byte); + ui->text_select_anchor_utf16 = anchor; + ui->text_select_focus_utf16 = focus; + ui->pending_input_selection_apply = false; + ui->state->caret_dirty = false; + } + 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 && !ui->text_select_mode) { + const int len_chars = Utf16CountFromUtf8Range(data->Buf, data->Buf + buf_len); + 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; + } + if (ClampInputBufferToUtf16Limit(data, static_cast(ui->state->max_text_length))) { + buf_len = std::max(0, data->BufTextLen); + } + const int cursor_byte = std::clamp(data->CursorPos, 0, buf_len); + const int selection_start_byte = + std::clamp(std::min(data->SelectionStart, data->SelectionEnd), 0, buf_len); + const int selection_end_byte = + std::clamp(std::max(data->SelectionStart, data->SelectionEnd), 0, buf_len); + const int cursor_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + cursor_byte); + ui->state->caret_byte_index = cursor_byte; + ui->state->caret_index = cursor_utf16; + ui->input_cursor_byte = cursor_byte; + ui->input_cursor_utf16 = cursor_utf16; + ui->input_selection_start_byte = selection_start_byte; + ui->input_selection_end_byte = selection_end_byte; + if (!ui->text_select_mode) { + ui->text_select_anchor_utf16 = cursor_utf16; + ui->text_select_focus_utf16 = cursor_utf16; + } + return 0; + } LOG_DEBUG(Lib_ImeDialog, ">> InputTextCallback: EventFlag={}, EventChar={}", data->EventFlag, data->EventChar); + if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter && + RejectInputCharByUtf16Limit(data, static_cast(ui->state->max_text_length))) { + return 1; + } + // Should we filter punctuation? if (ui->state->is_numeric && (data->EventChar < '0' || data->EventChar > '9') && - data->EventChar != '\b' && data->EventChar != ',' && data->EventChar != '.') { + data->EventChar != '\b' && data->EventChar != ',' && data->EventChar != '-' && + data->EventChar != '.') { LOG_INFO(Lib_ImeDialog, "InputTextCallback: rejecting non-digit char '{}'", static_cast(data->EventChar)); 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); @@ -398,6 +2166,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 a0e03a523..a72da5828 100644 --- a/src/core/libraries/ime/ime_dialog_ui.h +++ b/src/core/libraries/ime/ime_dialog_ui.h @@ -9,26 +9,49 @@ #include "common/cstring.h" #include "common/types.h" #include "core/libraries/ime/ime_dialog.h" +#include "core/libraries/ime/ime_kb_layout.h" #include "imgui/imgui_layer.h" 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; + OrbisImeExtOption ext_option = OrbisImeExtOption::DEFAULT; + OrbisImeDisableDevice disable_device = OrbisImeDisableDevice::DEFAULT; + OrbisImePanelPriority panel_priority = OrbisImePanelPriority::Default; + Libraries::Ime::ImeStyleConfig style_config{}; s32 user_id{}; bool is_multi_line{}; bool is_numeric{}; + bool fixed_position{}; OrbisImeType type{}; + OrbisImeLanguage supported_languages{}; OrbisImeEnterLabel enter_label{}; OrbisImeTextFilter text_filter{}; OrbisImeExtKeyboardFilter keyboard_filter{}; u32 max_text_length{}; char16_t* text_buffer{}; + std::vector original_text; std::vector title; std::vector placeholder; @@ -48,8 +71,10 @@ public: ImeDialogState(ImeDialogState&& other) noexcept; ImeDialogState& operator=(ImeDialogState&& other); - bool CopyTextToOrbisBuffer(); + bool CopyTextToOrbisBuffer(bool use_original); bool CallTextFilter(); + bool NormalizeNewlines(); + bool ClampCurrentTextToMaxLen(); private: bool CallKeyboardFilter(const OrbisImeKeycode* src_keycode, u16* out_keycode, u32* out_status); @@ -61,11 +86,83 @@ private: }; class ImeDialogUi final : public ImGui::Layer { + enum class PanelSelectionTarget : u8 { + Input = 0, + Prediction = 1, + Close = 2, + Keyboard = 3, + }; + + enum class EditMenuPopup : u8 { + None = 0, + Main = 1, + Actions = 2, + }; + ImeDialogState* state{}; OrbisImeDialogStatus* status{}; OrbisImeDialogResult* result{}; bool first_render = true; + bool accept_armed = false; + bool native_input_active = false; + bool pointer_navigation_active = true; + EditMenuPopup edit_menu_popup = EditMenuPopup::None; + bool menu_activate_armed = true; + bool l2_shortcut_armed = true; + bool request_input_focus = false; + bool request_input_select_all = false; + bool text_select_mode = false; + bool pending_input_selection_apply = false; + bool prev_virtual_cross_down = false; + bool prev_virtual_lstick_left_down = false; + bool prev_virtual_lstick_right_down = false; + bool prev_virtual_lstick_up_down = false; + bool prev_virtual_lstick_down_down = false; + int left_stick_repeat_dir = 0; + double left_stick_next_repeat_time = 0.0; + double virtual_cross_next_repeat_time = 0.0; + double virtual_triangle_next_repeat_time = 0.0; + u32 prev_virtual_buttons = 0; + bool prev_virtual_square_down = false; + bool prev_virtual_l1_down = false; + bool prev_virtual_r1_down = false; + bool prev_virtual_dpad_left_down = false; + bool prev_virtual_dpad_right_down = false; + bool prev_virtual_dpad_up_down = false; + bool prev_virtual_dpad_down_down = false; + double virtual_square_next_repeat_time = 0.0; + double virtual_l1_next_repeat_time = 0.0; + double virtual_r1_next_repeat_time = 0.0; + double virtual_dpad_left_next_repeat_time = 0.0; + double virtual_dpad_right_next_repeat_time = 0.0; + double virtual_dpad_up_next_repeat_time = 0.0; + double virtual_dpad_down_next_repeat_time = 0.0; + Libraries::Ime::ImeEdgeWrapNavState panel_vertical_nav_state{}; + bool panel_position_initialized = false; + bool panel_layout_anchor_initialized = false; + bool panel_drag_active = false; + bool gamepad_input_capture_active = false; + ImVec2 panel_position{}; + ImVec2 panel_layout_anchor{}; + ImVec2 panel_drag_press_offset{}; + int input_cursor_utf16 = 0; + int input_cursor_byte = 0; + int input_selection_start_byte = 0; + int input_selection_end_byte = 0; + int text_select_anchor_utf16 = -1; + int text_select_focus_utf16 = -1; + int top_virtual_col = 0; + PanelSelectionTarget panel_selection = PanelSelectionTarget::Keyboard; + int pending_keyboard_row = -1; + int pending_keyboard_col = -1; + int last_keyboard_selected_row = 0; + int last_keyboard_selected_col = 0; + int edit_menu_index = 0; + Libraries::Ime::ImeKbLayoutSelection kb_layout_selection{}; + Libraries::Ime::ImeKbLayoutSelection last_nav_layout_selection{}; + bool nav_layout_selection_initialized = false; + Libraries::Ime::ImeKbLayoutFamily kb_alpha_family = Libraries::Ime::ImeKbLayoutFamily::Latin; std::mutex draw_mutex; public: @@ -79,10 +176,13 @@ public: void Draw() override; private: + void FinishDialog(OrbisImeDialogEndStatus endstatus, bool restore_original, const char* reason); void Free(); - void DrawInputText(); - void DrawMultiLineInputText(); + bool DrawInputText(const Libraries::Ime::ImePanelMetrics& metrics, + bool pointer_selection_enabled); + bool DrawMultiLineInputText(const Libraries::Ime::ImePanelMetrics& metrics, + bool pointer_selection_enabled); 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 000000000..597ee23ee --- /dev/null +++ b/src/core/libraries/ime/ime_kb_layout.cpp @@ -0,0 +1,2267 @@ +// 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 +#include +#include +#include +#include + +#include "core/debug_state.h" +#include "core/libraries/ime/ime_ui_shared.h" + +namespace Libraries::Ime { +namespace { + +#define KEY(r, c, label, action) \ + ImeKbKeySpec { \ + r, c, 1, 1, label, nullptr, ImeKbKeyAction::action, ImeKbKeyGlyph::None \ + } +#define KEYHOT(r, c, label, hotkey, action) \ + ImeKbKeySpec { \ + r, c, 1, 1, label, hotkey, ImeKbKeyAction::action, ImeKbKeyGlyph::None \ + } +#define KEYSPAN(r, c, cs, rs, label, action) \ + ImeKbKeySpec { \ + r, c, cs, rs, label, nullptr, ImeKbKeyAction::action, ImeKbKeyGlyph::None \ + } +#define KEYSPANHOT(r, c, cs, rs, label, hotkey, action) \ + ImeKbKeySpec { \ + r, c, cs, rs, label, hotkey, ImeKbKeyAction::action, ImeKbKeyGlyph::None \ + } +#define KEYGLYPH(r, c, glyph, action) \ + ImeKbKeySpec { \ + r, c, 1, 1, nullptr, nullptr, ImeKbKeyAction::action, ImeKbKeyGlyph::glyph \ + } +#define KEYHOTGLYPH(r, c, hotkey, glyph, action) \ + ImeKbKeySpec { \ + r, c, 1, 1, nullptr, hotkey, ImeKbKeyAction::action, ImeKbKeyGlyph::glyph \ + } +#define KEYSPANGLYPH(r, c, cs, rs, glyph, action) \ + ImeKbKeySpec { \ + r, c, cs, rs, nullptr, nullptr, ImeKbKeyAction::action, ImeKbKeyGlyph::glyph \ + } +#define KEYSPANHOTGLYPH(r, c, cs, rs, hotkey, glyph, action) \ + ImeKbKeySpec { \ + r, c, cs, rs, nullptr, hotkey, ImeKbKeyAction::action, ImeKbKeyGlyph::glyph \ + } + +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.042f; +constexpr float kTypingKeyLabelScale = 1.60f; +constexpr float kFunctionKeyLabelScale = 1.26f; +constexpr float kHotkeyLabelScale = 0.74f; +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 kSpecialsKeyRows = 7; +constexpr int kKeyCols = 10; +constexpr char kCurrencyDollar[] = "$"; +constexpr char kCurrencyEuro[] = "\xE2\x82\xAC"; +constexpr char kCurrencyPound[] = "\xC2\xA3"; +constexpr char kCurrencyYen[] = "\xC2\xA5"; +constexpr char kCurrencyWon[] = "\xE2\x82\xA9"; +constexpr char kHotkeyTriangleRounded[] = "\xE2\x96\xB3"; +constexpr char kHotkeySquareRounded[] = "\xE2\x96\xA2"; +constexpr char kHotkeyL2TriangleRounded[] = "L2+\xE2\x96\xB3"; +constexpr char kArrowLabelUp[] = "\xE2\x96\xB2"; +constexpr char kArrowLabelRight[] = "\xE2\x96\xB6"; +constexpr char kArrowLabelDown[] = "\xE2\x96\xBC"; +constexpr char kArrowLabelLeft[] = "\xE2\x97\x80"; +constexpr char kLanguageSwitchLabel[] = "\xE2\x8C\xA8"; +constexpr char kControlModeGyroLabel[] = "\xE2\x98\x89/\xE2\x8C\x96"; +// SCE_IME keycodes follow USB HID usage IDs. +constexpr u16 kCodeSym1 = 0x003A; // SCE_IME_KEYCODE_F1 +constexpr u16 kCodeAccents = 0x003B; // SCE_IME_KEYCODE_F2 +constexpr u16 kCodeLetters = 0x003C; // SCE_IME_KEYCODE_F3 +constexpr u16 kCodeKeyboard = 0x003D; // SCE_IME_KEYCODE_F4 +constexpr u16 kCodeSettings = 0x003E; // SCE_IME_KEYCODE_F5 +constexpr u16 kCodeOptions = 0x0076; // SCE_IME_KEYCODE_MENU +constexpr u16 kCodeArrowUp = 0x0052; // SCE_IME_KEYCODE_UPARROW +constexpr u16 kCodeArrowDown = 0x0051; // SCE_IME_KEYCODE_DOWNARROW +constexpr u16 kCodeArrowLeft = 0x0050; // SCE_IME_KEYCODE_LEFTARROW +constexpr u16 kCodeArrowRight = 0x004F; // SCE_IME_KEYCODE_RIGHTARROW +constexpr u16 kCodeSpace = 0x002C; // SCE_IME_KEYCODE_SPACEBAR +constexpr u16 kCodeBackspace = 0x002A; // SCE_IME_KEYCODE_BACKSPACE +constexpr u16 kCodeReturn = 0x0028; // SCE_IME_KEYCODE_RETURN +constexpr u16 kCodeLeftShift = 0x00E1; // SCE_IME_KEYCODE_LEFTSHIFT +constexpr u64 kMaskDollarPreferred = static_cast(OrbisImeLanguage::ENGLISH_US) | + static_cast(OrbisImeLanguage::SPANISH_LA) | + static_cast(OrbisImeLanguage::PORTUGUESE_BR); +constexpr u64 kMaskPoundPreferred = static_cast(OrbisImeLanguage::ENGLISH_GB); +constexpr u64 kMaskYenPreferred = static_cast(OrbisImeLanguage::JAPANESE); +constexpr u64 kMaskWonPreferred = static_cast(OrbisImeLanguage::KOREAN); + +enum class ImeKbCurrencyProfile : u8 { + Euro = 0, + Dollar = 1, + Pound = 2, + Yen = 3, + Won = 4, +}; + +u16 Utf8FirstCodeUnit(const char* text) { + if (!text || text[0] == '\0') { + return 0; + } + const unsigned char c0 = static_cast(text[0]); + if ((c0 & 0x80u) == 0) { + return static_cast(c0); + } + if ((c0 & 0xE0u) == 0xC0u && text[1] != '\0') { + const unsigned char c1 = static_cast(text[1]); + if ((c1 & 0xC0u) == 0x80u) { + return static_cast(((c0 & 0x1Fu) << 6) | (c1 & 0x3Fu)); + } + return 0; + } + if ((c0 & 0xF0u) == 0xE0u && text[1] != '\0' && text[2] != '\0') { + const unsigned char c1 = static_cast(text[1]); + const unsigned char c2 = static_cast(text[2]); + if ((c1 & 0xC0u) == 0x80u && (c2 & 0xC0u) == 0x80u) { + return static_cast(((c0 & 0x0Fu) << 12) | ((c1 & 0x3Fu) << 6) | (c2 & 0x3Fu)); + } + return 0; + } + return 0; +} + +u16 ResolveAsciiHidKeycode(u16 code_unit) { + if (code_unit >= 'a' && code_unit <= 'z') { + return static_cast(0x0004 + (code_unit - 'a')); // SCE_IME_KEYCODE_A... + } + if (code_unit >= 'A' && code_unit <= 'Z') { + return static_cast(0x0004 + (code_unit - 'A')); // SCE_IME_KEYCODE_A... + } + if (code_unit >= '1' && code_unit <= '9') { + return static_cast(0x001E + (code_unit - '1')); // SCE_IME_KEYCODE_1... + } + if (code_unit == '0') { + return 0x0027; // SCE_IME_KEYCODE_0 + } + + switch (code_unit) { + case ' ': + return kCodeSpace; + case '\b': + return kCodeBackspace; + case '\r': + case '\n': + return kCodeReturn; + case '-': + case '_': + return 0x002D; // SCE_IME_KEYCODE_MINUS + case '=': + case '+': + return 0x002E; // SCE_IME_KEYCODE_EQUAL + case '[': + case '{': + return 0x002F; // SCE_IME_KEYCODE_LEFTBRACKET + case ']': + case '}': + return 0x0030; // SCE_IME_KEYCODE_RIGHTBRACKET + case '\\': + case '|': + return 0x0031; // SCE_IME_KEYCODE_BACKSLASH + case ';': + case ':': + return 0x0033; // SCE_IME_KEYCODE_SEMICOLON + case '\'': + case '"': + return 0x0034; // SCE_IME_KEYCODE_SINGLEQUOTE + case '`': + case '~': + return 0x0035; // SCE_IME_KEYCODE_BACKQUOTE + case ',': + case '<': + return 0x0036; // SCE_IME_KEYCODE_COMMA + case '.': + case '>': + return 0x0037; // SCE_IME_KEYCODE_PERIOD + case '/': + case '?': + return 0x0038; // SCE_IME_KEYCODE_SLASH + case '!': + return 0x001E; // Shift+1 + case '@': + return 0x001F; // Shift+2 + case '#': + return 0x0020; // Shift+3 + case '$': + return 0x0021; // Shift+4 + case '%': + return 0x0022; // Shift+5 + case '^': + return 0x0023; // Shift+6 + case '&': + return 0x0024; // Shift+7 + case '*': + return 0x0025; // Shift+8 + case '(': + return 0x0026; // Shift+9 + case ')': + return 0x0027; // Shift+0 + default: + return 0; + } +} + +u16 ResolveImeKeycode(const ImeKbKeySpec& key, const char* label_override = nullptr) { + switch (key.action) { + case ImeKbKeyAction::Character: { + const u16 code_unit = Utf8FirstCodeUnit(label_override ? label_override : key.label); + return ResolveAsciiHidKeycode(code_unit); + } + case ImeKbKeyAction::Space: + return kCodeSpace; + case ImeKbKeyAction::Backspace: + return kCodeBackspace; + case ImeKbKeyAction::NewLine: + case ImeKbKeyAction::Done: + return kCodeReturn; + case ImeKbKeyAction::ArrowUp: + return kCodeArrowUp; + case ImeKbKeyAction::ArrowDown: + return kCodeArrowDown; + case ImeKbKeyAction::ArrowLeft: + case ImeKbKeyAction::PagePrev: + return kCodeArrowLeft; + case ImeKbKeyAction::ArrowRight: + case ImeKbKeyAction::PageNext: + return kCodeArrowRight; + case ImeKbKeyAction::SymbolsMode: { + const char* mode_label = label_override ? label_override : key.label; + if (mode_label && std::strcmp(mode_label, "ABC") == 0) { + return kCodeLetters; + } + return kCodeSym1; + } + case ImeKbKeyAction::SpecialsMode: { + const char* mode_label = label_override ? label_override : key.label; + if (mode_label && std::strcmp(mode_label, "ABC") == 0) { + return kCodeLetters; + } + return kCodeAccents; + } + case ImeKbKeyAction::Keyboard: + return kCodeKeyboard; + case ImeKbKeyAction::Menu: + return kCodeOptions; + case ImeKbKeyAction::Settings: + return kCodeSettings; + case ImeKbKeyAction::Shift: + return kCodeLeftShift; + case ImeKbKeyAction::None: + default: + return 0; + } +} + +char16_t ResolveImeCharacter(const ImeKbKeySpec& key, const char* label_override = nullptr) { + switch (key.action) { + case ImeKbKeyAction::Character: + return static_cast( + Utf8FirstCodeUnit(label_override ? label_override : key.label)); + case ImeKbKeyAction::Space: + return u' '; + case ImeKbKeyAction::NewLine: + case ImeKbKeyAction::Done: + return u'\r'; + default: + return u'\0'; + } +} + +ImeKbCurrencyProfile ResolveCurrencyProfile(OrbisImeLanguage supported_languages) { + const u64 mask = static_cast(supported_languages); + if ((mask & kMaskWonPreferred) != 0) { + return ImeKbCurrencyProfile::Won; + } + if ((mask & kMaskYenPreferred) != 0) { + return ImeKbCurrencyProfile::Yen; + } + if ((mask & kMaskPoundPreferred) != 0) { + return ImeKbCurrencyProfile::Pound; + } + if ((mask & kMaskDollarPreferred) != 0) { + return ImeKbCurrencyProfile::Dollar; + } + return ImeKbCurrencyProfile::Euro; +} + +const char* GetCurrencySlotLabel(ImeKbCurrencyProfile profile, int slot) { + switch (profile) { + case ImeKbCurrencyProfile::Dollar: { + static constexpr std::array kLabels = { + kCurrencyDollar, + kCurrencyEuro, + kCurrencyPound, + kCurrencyYen, + }; + return kLabels[slot]; + } + case ImeKbCurrencyProfile::Pound: { + static constexpr std::array kLabels = { + kCurrencyPound, + kCurrencyEuro, + kCurrencyDollar, + kCurrencyYen, + }; + return kLabels[slot]; + } + case ImeKbCurrencyProfile::Yen: { + static constexpr std::array kLabels = { + kCurrencyYen, + kCurrencyEuro, + kCurrencyDollar, + kCurrencyPound, + }; + return kLabels[slot]; + } + case ImeKbCurrencyProfile::Won: { + static constexpr std::array kLabels = { + kCurrencyWon, + kCurrencyEuro, + kCurrencyDollar, + kCurrencyYen, + }; + return kLabels[slot]; + } + case ImeKbCurrencyProfile::Euro: + default: { + static constexpr std::array kLabels = { + kCurrencyEuro, + kCurrencyPound, + kCurrencyYen, + kCurrencyWon, + }; + return kLabels[slot]; + } + } +} + +const char* ResolveSymbolOverrideLabel(const ImeKbLayoutSelection& selection, + OrbisImeLanguage supported_languages, + const ImeKbKeySpec& key, const char* fallback_label) { + if (selection.family != ImeKbLayoutFamily::Symbols || key.action != ImeKbKeyAction::Character || + key.row != 3) { + return fallback_label; + } + + const bool is_page1 = (selection.page % 2) == 0; + const ImeKbCurrencyProfile profile = ResolveCurrencyProfile(supported_languages); + + // Keep one locale-aware currency slot on symbols page 1 and preserve the + // rest of the page for PS4 punctuation/quote marks. + if (is_page1 && key.col == 5) { + return GetCurrencySlotLabel(profile, 0); + } + return fallback_label; +} + +const char* ResolveShiftOverrideLabel(const ImeKbLayoutSelection& selection, + const ImeKbKeySpec& key, const char* fallback_label) { + if (key.action != ImeKbKeyAction::Shift || !fallback_label || fallback_label[0] == '\0') { + return fallback_label; + } + + switch (selection.case_state) { + case ImeKbCaseState::Upper: + case ImeKbCaseState::CapsLock: + return "SHIFT"; + case ImeKbCaseState::Lower: + default: + return "shift"; + } +} + +ImeKbKeyGlyph ResolveShiftOverrideGlyph(const ImeKbLayoutSelection& selection, + const ImeKbKeySpec& key, ImeKbKeyGlyph fallback_glyph) { + if (key.action != ImeKbKeyAction::Shift) { + return fallback_glyph; + } + + switch (selection.case_state) { + case ImeKbCaseState::Upper: + return ImeKbKeyGlyph::ShiftFilled; + case ImeKbCaseState::CapsLock: + return ImeKbKeyGlyph::CapsLockFilled; + case ImeKbCaseState::Lower: + default: + return ImeKbKeyGlyph::ShiftOutline; + } +} + +template +constexpr ImeKbLayoutModel MakeLayoutModel( + const std::array& keys, const u8 rows = static_cast(kKeyRows), + const u8 function_rows = static_cast(ImeSelectionGridIndex::DefaultFunctionRows)) { + return ImeKbLayoutModel{keys.data(), N, static_cast(kKeyCols), rows, function_rows}; +} + +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"; + } +} + +constexpr std::array kLatinLowerKeys = {{ + KEY(0, 0, "1", Character), + KEY(0, 1, "2", Character), + KEY(0, 2, "3", Character), + KEY(0, 3, "4", Character), + KEY(0, 4, "5", Character), + KEY(0, 5, "6", Character), + KEY(0, 6, "7", Character), + KEY(0, 7, "8", Character), + KEY(0, 8, "9", Character), + KEY(0, 9, "0", Character), + + KEY(1, 0, "q", Character), + KEY(1, 1, "w", Character), + KEY(1, 2, "e", Character), + KEY(1, 3, "r", Character), + KEY(1, 4, "t", Character), + KEY(1, 5, "y", Character), + KEY(1, 6, "u", Character), + KEY(1, 7, "i", Character), + KEY(1, 8, "o", Character), + KEY(1, 9, "p", Character), + + KEY(2, 0, "a", Character), + KEY(2, 1, "s", Character), + KEY(2, 2, "d", Character), + KEY(2, 3, "f", Character), + KEY(2, 4, "g", Character), + KEY(2, 5, "h", Character), + KEY(2, 6, "j", Character), + KEY(2, 7, "k", Character), + KEY(2, 8, "l", Character), + KEY(2, 9, "'", Character), + + KEY(3, 0, "z", Character), + KEY(3, 1, "x", Character), + KEY(3, 2, "c", Character), + KEY(3, 3, "v", Character), + KEY(3, 4, "b", Character), + KEY(3, 5, "n", Character), + KEY(3, 6, "m", Character), + KEY(3, 7, ",", Character), + KEY(3, 8, ".", Character), + KEY(3, 9, "?", Character), + + KEYHOTGLYPH(4, 0, "L2", ShiftOutline, Shift), + KEYHOT(4, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(4, 2, "\xC3\xA0", "L3", SpecialsMode), + KEYSPANHOT(4, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(4, 7, nullptr, None), + KEYSPANHOTGLYPH(4, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(5, 0, ArrowDown, ArrowDown), + KEYGLYPH(5, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(5, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(5, 3, "R1", ArrowRight, ArrowRight), + KEY(5, 4, kLanguageSwitchLabel, Keyboard), + KEY(5, 5, "...", Menu), + KEYHOT(5, 6, kControlModeGyroLabel, "R3", Settings), + KEY(5, 7, nullptr, None), + KEYSPANHOT(5, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kLatinUpperKeys = {{ + KEY(0, 0, "1", Character), + KEY(0, 1, "2", Character), + KEY(0, 2, "3", Character), + KEY(0, 3, "4", Character), + KEY(0, 4, "5", Character), + KEY(0, 5, "6", Character), + KEY(0, 6, "7", Character), + KEY(0, 7, "8", Character), + KEY(0, 8, "9", Character), + KEY(0, 9, "0", Character), + + KEY(1, 0, "Q", Character), + KEY(1, 1, "W", Character), + KEY(1, 2, "E", Character), + KEY(1, 3, "R", Character), + KEY(1, 4, "T", Character), + KEY(1, 5, "Y", Character), + KEY(1, 6, "U", Character), + KEY(1, 7, "I", Character), + KEY(1, 8, "O", Character), + KEY(1, 9, "P", Character), + + KEY(2, 0, "A", Character), + KEY(2, 1, "S", Character), + KEY(2, 2, "D", Character), + KEY(2, 3, "F", Character), + KEY(2, 4, "G", Character), + KEY(2, 5, "H", Character), + KEY(2, 6, "J", Character), + KEY(2, 7, "K", Character), + KEY(2, 8, "L", Character), + KEY(2, 9, "\"", Character), + + KEY(3, 0, "Z", Character), + KEY(3, 1, "X", Character), + KEY(3, 2, "C", Character), + KEY(3, 3, "V", Character), + KEY(3, 4, "B", Character), + KEY(3, 5, "N", Character), + KEY(3, 6, "M", Character), + KEY(3, 7, "-", Character), + KEY(3, 8, "_", Character), + KEY(3, 9, "/", Character), + + KEYHOTGLYPH(4, 0, "L2", ShiftOutline, Shift), + KEYHOT(4, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(4, 2, "\xC3\xA0", "L3", SpecialsMode), + KEYSPANHOT(4, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(4, 7, nullptr, None), + KEYSPANHOTGLYPH(4, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(5, 0, ArrowDown, ArrowDown), + KEYGLYPH(5, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(5, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(5, 3, "R1", ArrowRight, ArrowRight), + KEY(5, 4, kLanguageSwitchLabel, Keyboard), + KEY(5, 5, "...", Menu), + KEYHOT(5, 6, kControlModeGyroLabel, "R3", Settings), + KEY(5, 7, nullptr, None), + KEYSPANHOT(5, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSymbolsPage1Keys = {{ + KEY(0, 0, "!", Character), + KEY(0, 1, "?", Character), + KEY(0, 2, "\"", Character), + KEY(0, 3, "'", Character), + KEY(0, 4, "#", Character), + KEY(0, 5, "%", Character), + KEY(0, 6, "(", Character), + KEY(0, 7, ")", Character), + KEY(0, 8, "()", Character), + KEY(0, 9, "/", Character), + + KEY(1, 0, "-", Character), + KEY(1, 1, "_", Character), + KEY(1, 2, ",", Character), + KEY(1, 3, ".", Character), + KEY(1, 4, ":", Character), + KEY(1, 5, ";", Character), + KEY(1, 6, "*", Character), + KEY(1, 7, "+", Character), + KEY(1, 8, "=", Character), + KEY(1, 9, "&", Character), + + KEY(2, 0, "<", Character), + KEY(2, 1, ">", Character), + KEY(2, 2, "@", Character), + KEY(2, 3, "[", Character), + KEY(2, 4, "]", Character), + KEY(2, 5, "[]", Character), + KEY(2, 6, "{", Character), + KEY(2, 7, "}", Character), + KEY(2, 8, "{}", Character), + KEYSPAN(2, 9, 1, 2, ">", PageNext), + + KEY(3, 0, "\\", Character), + KEY(3, 1, "|", Character), + KEY(3, 2, "^", Character), + KEY(3, 3, "`", Character), + KEY(3, 4, "$", Character), + KEY(3, 5, "\xE2\x82\xAC", Character), + KEY(3, 6, "\xC2\xB4", Character), + KEY(3, 7, "\xE2\x80\x98", Character), + KEY(3, 8, "\xE2\x80\x99", Character), + + KEYHOT(4, 1, "ABC", kHotkeyL2TriangleRounded, SymbolsMode), + KEYSPANHOT(4, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEYSPANHOTGLYPH(4, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(5, 0, ArrowDown, ArrowDown), + KEYGLYPH(5, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(5, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(5, 3, "R1", ArrowRight, ArrowRight), + KEY(5, 4, kLanguageSwitchLabel, Keyboard), + KEY(5, 5, "...", Menu), + KEYHOT(5, 6, kControlModeGyroLabel, "R3", Settings), + KEY(5, 7, nullptr, None), + KEYSPANHOT(5, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSymbolsPage2Keys = {{ + KEY(0, 0, "\xE2\x80\x9A", Character), + KEY(0, 1, "\xE2\x80\x9C", Character), + KEY(0, 2, "\xE2\x80\x9D", Character), + KEY(0, 3, "\xE2\x80\x9E", Character), + KEY(0, 4, "~", Character), + KEY(0, 5, "\xC2\xA1", Character), + KEY(0, 6, "\xC2\xA1!", Character), + KEY(0, 7, "\xC2\xBF", Character), + KEY(0, 8, "\xC2\xBF?", Character), + KEY(0, 9, "\xE2\x80\xB9", Character), + + KEY(1, 0, "\xE2\x80\xBA", Character), + KEY(1, 1, "\xC2\xAB", Character), + KEY(1, 2, "\xC2\xBB", Character), + KEY(1, 3, "\xC2\xB0", Character), + KEY(1, 4, "\xC2\xAA", Character), + KEY(1, 5, "\xC2\xBA", Character), + KEY(1, 6, "\xC3\x97", Character), + KEY(1, 7, "\xC3\xB7", Character), + KEY(1, 8, "\xC2\xA4", Character), + KEY(1, 9, "\xC2\xA2", Character), + + KEY(2, 0, "\xC2\xA5", Character), + KEY(2, 1, "\xC2\xA3", Character), + KEY(2, 2, "\xE2\x82\xA9", Character), + KEY(2, 3, "\xC2\xA7", Character), + KEY(2, 4, "\xC2\xA6", Character), + KEY(2, 5, "\xC2\xB5", Character), + KEY(2, 6, "\xC2\xAC", Character), + KEY(2, 7, "\xC2\xB9", Character), + KEY(2, 8, "\xC2\xB2", Character), + KEYSPAN(2, 9, 1, 2, "<", PagePrev), + + KEY(3, 0, "\xC2\xB3", Character), + KEY(3, 1, "\xC2\xBC", Character), + KEY(3, 2, "\xC2\xBD", Character), + KEY(3, 3, "\xC2\xBE", Character), + KEY(3, 4, "\xE2\x84\x96", Character), + KEY(3, 5, "\xC2\xB7", Character), + KEY(3, 6, nullptr, None), + KEY(3, 7, nullptr, None), + KEY(3, 8, nullptr, None), + + KEYHOT(4, 1, "ABC", kHotkeyL2TriangleRounded, SymbolsMode), + KEYSPANHOT(4, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEYSPANHOTGLYPH(4, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(5, 0, ArrowDown, ArrowDown), + KEYGLYPH(5, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(5, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(5, 3, "R1", ArrowRight, ArrowRight), + KEY(5, 4, kLanguageSwitchLabel, Keyboard), + KEY(5, 5, "...", Menu), + KEYHOT(5, 6, kControlModeGyroLabel, "R3", Settings), + KEY(5, 7, nullptr, None), + KEYSPANHOT(5, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSpecialsPage1Keys = {{ + KEY(0, 0, "à", Character), + KEY(0, 1, "á", Character), + KEY(0, 2, "â", Character), + KEY(0, 3, "ã", Character), + KEY(0, 4, "ä", Character), + KEY(0, 5, "å", Character), + KEY(0, 6, "ą", Character), + KEY(0, 7, "æ", Character), + KEY(0, 8, "ç", Character), + KEY(0, 9, "ć", Character), + + KEY(1, 0, "è", Character), + KEY(1, 1, "é", Character), + KEY(1, 2, "ê", Character), + KEY(1, 3, "ë", Character), + KEY(1, 4, "ę", Character), + KEY(1, 5, "ğ", Character), + KEY(1, 6, "ì", Character), + KEY(1, 7, "í", Character), + KEY(1, 8, "î", Character), + KEY(1, 9, "ï", Character), + + KEY(2, 0, "ı", Character), + KEY(2, 1, "ł", Character), + KEY(2, 2, "ñ", Character), + KEY(2, 3, "ń", Character), + KEY(2, 4, "ò", Character), + KEY(2, 5, "ó", Character), + KEY(2, 6, "ô", Character), + KEY(2, 7, "õ", Character), + KEY(2, 8, "ö", Character), + KEY(2, 9, "ø", Character), + + KEY(3, 0, "œ", Character), + KEY(3, 1, "ś", Character), + KEY(3, 2, "ş", Character), + KEY(3, 3, "š", Character), + KEY(3, 4, "ß", Character), + KEY(3, 5, "ù", Character), + KEY(3, 6, "ú", Character), + KEY(3, 7, "û", Character), + KEY(3, 8, "ü", Character), + KEY(3, 9, "ý", Character), + + KEY(4, 0, "ÿ", Character), + KEY(4, 1, "ź", Character), + KEY(4, 2, "ż", Character), + KEY(4, 3, "ž", Character), + KEY(4, 4, "ð", Character), + KEY(4, 5, "þ", Character), + KEY(4, 6, nullptr, None), + KEY(4, 7, nullptr, None), + KEY(4, 8, nullptr, None), + KEY(4, 9, nullptr, None), + + KEYHOTGLYPH(5, 0, "L2", ShiftOutline, Shift), + KEYHOT(5, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(5, 2, "ABC", "L3", SpecialsMode), + KEYSPANHOT(5, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(5, 7, nullptr, None), + KEYSPANHOTGLYPH(5, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(6, 0, ArrowDown, ArrowDown), + KEYGLYPH(6, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(6, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(6, 3, "R1", ArrowRight, ArrowRight), + KEY(6, 4, kLanguageSwitchLabel, Keyboard), + KEY(6, 5, "...", Menu), + KEYHOT(6, 6, kControlModeGyroLabel, "R3", Settings), + KEY(6, 7, nullptr, None), + KEYSPANHOT(6, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSpecialsPage1UpperKeys = {{ + KEY(0, 0, "À", Character), + KEY(0, 1, "Á", Character), + KEY(0, 2, "Â", Character), + KEY(0, 3, "Ã", Character), + KEY(0, 4, "Ä", Character), + KEY(0, 5, "Å", Character), + KEY(0, 6, "Ą", Character), + KEY(0, 7, "Æ", Character), + KEY(0, 8, "Ç", Character), + KEY(0, 9, "Ć", Character), + + KEY(1, 0, "È", Character), + KEY(1, 1, "É", Character), + KEY(1, 2, "Ê", Character), + KEY(1, 3, "Ë", Character), + KEY(1, 4, "Ę", Character), + KEY(1, 5, "Ğ", Character), + KEY(1, 6, "Ì", Character), + KEY(1, 7, "Í", Character), + KEY(1, 8, "Î", Character), + KEY(1, 9, "Ï", Character), + + KEY(2, 0, "İ", Character), + KEY(2, 1, "Ł", Character), + KEY(2, 2, "Ñ", Character), + KEY(2, 3, "Ń", Character), + KEY(2, 4, "Ò", Character), + KEY(2, 5, "Ó", Character), + KEY(2, 6, "Ô", Character), + KEY(2, 7, "Õ", Character), + KEY(2, 8, "Ö", Character), + KEY(2, 9, "Ø", Character), + + KEY(3, 0, "Œ", Character), + KEY(3, 1, "Ś", Character), + KEY(3, 2, "Ş", Character), + KEY(3, 3, "Š", Character), + KEY(3, 4, "ß", Character), + KEY(3, 5, "Ù", Character), + KEY(3, 6, "Ú", Character), + KEY(3, 7, "Û", Character), + KEY(3, 8, "Ü", Character), + KEY(3, 9, "Ý", Character), + + KEY(4, 0, "Ÿ", Character), + KEY(4, 1, "Ź", Character), + KEY(4, 2, "Ż", Character), + KEY(4, 3, "Ž", Character), + KEY(4, 4, "Ð", Character), + KEY(4, 5, "Þ", Character), + KEY(4, 6, nullptr, None), + KEY(4, 7, nullptr, None), + KEY(4, 8, nullptr, None), + KEY(4, 9, nullptr, None), + + KEYHOTGLYPH(5, 0, "L2", ShiftOutline, Shift), + KEYHOT(5, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(5, 2, "ABC", "L3", SpecialsMode), + KEYSPANHOT(5, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(5, 7, nullptr, None), + KEYSPANHOTGLYPH(5, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(6, 0, ArrowDown, ArrowDown), + KEYGLYPH(6, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(6, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(6, 3, "R1", ArrowRight, ArrowRight), + KEY(6, 4, kLanguageSwitchLabel, Keyboard), + KEY(6, 5, "...", Menu), + KEYHOT(6, 6, kControlModeGyroLabel, "R3", Settings), + KEY(6, 7, nullptr, None), + KEYSPANHOT(6, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSpecialsPage2Keys = {{ + KEY(0, 0, "ÿ", Character), + KEY(0, 1, "ź", Character), + KEY(0, 2, "ż", Character), + KEY(0, 3, "ž", Character), + KEY(0, 4, "ð", Character), + KEY(0, 5, "þ", Character), + KEY(0, 6, nullptr, None), + KEY(0, 7, nullptr, None), + KEY(0, 8, nullptr, None), + KEY(0, 9, nullptr, None), + + KEY(1, 0, nullptr, None), + KEY(1, 1, nullptr, None), + KEY(1, 2, nullptr, None), + KEY(1, 3, nullptr, None), + KEY(1, 4, nullptr, None), + KEY(1, 5, nullptr, None), + KEY(1, 6, nullptr, None), + KEY(1, 7, nullptr, None), + KEY(1, 8, nullptr, None), + KEY(1, 9, nullptr, None), + + KEY(2, 0, nullptr, None), + KEY(2, 1, nullptr, None), + KEY(2, 2, nullptr, None), + KEY(2, 3, nullptr, None), + KEY(2, 4, nullptr, None), + KEY(2, 5, nullptr, None), + KEY(2, 6, nullptr, None), + KEY(2, 7, nullptr, None), + KEY(2, 8, nullptr, None), + KEY(2, 9, nullptr, None), + + KEY(3, 0, nullptr, None), + KEY(3, 1, nullptr, None), + KEY(3, 2, nullptr, None), + KEY(3, 3, nullptr, None), + KEY(3, 4, nullptr, None), + KEY(3, 5, nullptr, None), + KEY(3, 6, nullptr, None), + KEY(3, 7, nullptr, None), + KEY(3, 8, nullptr, None), + KEY(3, 9, nullptr, None), + + KEYHOTGLYPH(5, 0, "L2", ShiftOutline, Shift), + KEYHOT(5, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(5, 2, "ABC", "L3", SpecialsMode), + KEYSPANHOT(5, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(5, 7, nullptr, None), + KEYSPANHOTGLYPH(5, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(6, 0, ArrowDown, ArrowDown), + KEYGLYPH(6, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(6, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(6, 3, "R1", ArrowRight, ArrowRight), + KEY(6, 4, kLanguageSwitchLabel, Keyboard), + KEY(6, 5, "...", Menu), + KEYHOT(6, 6, kControlModeGyroLabel, "R3", Settings), + KEY(6, 7, nullptr, None), + KEYSPANHOT(6, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr std::array kSpecialsPage2UpperKeys = {{ + KEY(0, 0, "Ÿ", Character), + KEY(0, 1, "Ź", Character), + KEY(0, 2, "Ż", Character), + KEY(0, 3, "Ž", Character), + KEY(0, 4, "Ð", Character), + KEY(0, 5, "Þ", Character), + KEY(0, 6, nullptr, None), + KEY(0, 7, nullptr, None), + KEY(0, 8, nullptr, None), + KEY(0, 9, nullptr, None), + + KEY(1, 0, nullptr, None), + KEY(1, 1, nullptr, None), + KEY(1, 2, nullptr, None), + KEY(1, 3, nullptr, None), + KEY(1, 4, nullptr, None), + KEY(1, 5, nullptr, None), + KEY(1, 6, nullptr, None), + KEY(1, 7, nullptr, None), + KEY(1, 8, nullptr, None), + KEY(1, 9, nullptr, None), + + KEY(2, 0, nullptr, None), + KEY(2, 1, nullptr, None), + KEY(2, 2, nullptr, None), + KEY(2, 3, nullptr, None), + KEY(2, 4, nullptr, None), + KEY(2, 5, nullptr, None), + KEY(2, 6, nullptr, None), + KEY(2, 7, nullptr, None), + KEY(2, 8, nullptr, None), + KEY(2, 9, nullptr, None), + + KEY(3, 0, nullptr, None), + KEY(3, 1, nullptr, None), + KEY(3, 2, nullptr, None), + KEY(3, 3, nullptr, None), + KEY(3, 4, nullptr, None), + KEY(3, 5, nullptr, None), + KEY(3, 6, nullptr, None), + KEY(3, 7, nullptr, None), + KEY(3, 8, nullptr, None), + KEY(3, 9, nullptr, None), + + KEYHOTGLYPH(5, 0, "L2", ShiftOutline, Shift), + KEYHOT(5, 1, "@#:", kHotkeyL2TriangleRounded, SymbolsMode), + KEYHOT(5, 2, "ABC", "L3", SpecialsMode), + KEYSPANHOT(5, 3, 4, 1, "Space", kHotkeyTriangleRounded, Space), + KEY(5, 7, nullptr, None), + KEYSPANHOTGLYPH(5, 8, 2, 1, kHotkeySquareRounded, Backspace, Backspace), + + KEYGLYPH(6, 0, ArrowDown, ArrowDown), + KEYGLYPH(6, 1, ArrowUp, ArrowUp), + KEYHOTGLYPH(6, 2, "L1", ArrowLeft, ArrowLeft), + KEYHOTGLYPH(6, 3, "R1", ArrowRight, ArrowRight), + KEY(6, 4, kLanguageSwitchLabel, Keyboard), + KEY(6, 5, "...", Menu), + KEYHOT(6, 6, kControlModeGyroLabel, "R3", Settings), + KEY(6, 7, nullptr, None), + KEYSPANHOT(6, 8, 2, 1, nullptr, "R2", Done), +}}; + +constexpr ImeKbLayoutModel kLatinLowerModel = MakeLayoutModel(kLatinLowerKeys); +constexpr ImeKbLayoutModel kLatinUpperModel = MakeLayoutModel(kLatinUpperKeys); +constexpr ImeKbLayoutModel kSymbolsPage1Model = MakeLayoutModel(kSymbolsPage1Keys); +constexpr ImeKbLayoutModel kSymbolsPage2Model = MakeLayoutModel(kSymbolsPage2Keys); +constexpr ImeKbLayoutModel kSpecialsPage1Model = + MakeLayoutModel(kSpecialsPage1Keys, static_cast(kSpecialsKeyRows)); +constexpr ImeKbLayoutModel kSpecialsPage2Model = + MakeLayoutModel(kSpecialsPage2Keys, static_cast(kSpecialsKeyRows)); +constexpr ImeKbLayoutModel kSpecialsPage1UpperModel = + MakeLayoutModel(kSpecialsPage1UpperKeys, static_cast(kSpecialsKeyRows)); +constexpr ImeKbLayoutModel kSpecialsPage2UpperModel = + MakeLayoutModel(kSpecialsPage2UpperKeys, static_cast(kSpecialsKeyRows)); + +constexpr std::array kTopPanelDefaultElements = {{ + ImeTopPanelElementSpec{ImeTopPanelElementId::Prediction, 0, 9}, + ImeTopPanelElementSpec{ImeTopPanelElementId::Close, 9, 1}, +}}; + +constexpr ImeTopPanelLayoutConfig kTopPanelDefaultLayout{ + kTopPanelDefaultElements.data(), + kTopPanelDefaultElements.size(), + 10, + static_cast(ImeSelectionGridIndex::DefaultTopPanelRow), + static_cast(ImeSelectionGridIndex::DefaultTopPanelRows), +}; + +const ImeKbLayoutModel& GetDefaultLayoutModel() { + return kLatinLowerModel; +} + +const ImeTopPanelLayoutConfig& GetDefaultTopPanelLayoutConfig() { + return kTopPanelDefaultLayout; +} + +#undef KEY +#undef KEYHOT +#undef KEYSPAN +#undef KEYSPANHOT +#undef KEYGLYPH +#undef KEYHOTGLYPH +#undef KEYSPANGLYPH +#undef KEYSPANHOTGLYPH + +} // namespace + +namespace { + +void AddImeKeyLabelGlyphs(ImFontGlyphRangesBuilder& builder, const char* label) { + if (!label || label[0] == '\0') { + return; + } + builder.AddText(label); +} + +void AddImeLayoutGlyphs(ImFontGlyphRangesBuilder& builder, const ImeKbLayoutModel& model, + const ImeKbLayoutSelection& selection, + const OrbisImeLanguage supported_languages) { + if (!model.keys || model.key_count == 0) { + return; + } + + for (std::size_t i = 0; i < model.key_count; ++i) { + const ImeKbKeySpec& key = model.keys[i]; + const bool is_done = key.action == ImeKbKeyAction::Done; + const char* label = key.label; + if (!is_done) { + label = ResolveSymbolOverrideLabel(selection, supported_languages, key, label); + label = ResolveShiftOverrideLabel(selection, key, label); + } + AddImeKeyLabelGlyphs(builder, label); + AddImeKeyLabelGlyphs(builder, key.hotkey_label); + } +} + +} // namespace + +void AddImeKeyboardGlyphsToFontRanges(ImFontGlyphRangesBuilder& builder) { + // Resolve labels via real layout selection paths so accent/symbol overrides stay complete. + constexpr std::array kLanguageMasks = { + static_cast(0), OrbisImeLanguage::ENGLISH_US, + OrbisImeLanguage::ENGLISH_GB, OrbisImeLanguage::JAPANESE, + OrbisImeLanguage::KOREAN, + }; + constexpr std::array kFamilies = { + ImeKbLayoutFamily::Latin, + ImeKbLayoutFamily::Symbols, + ImeKbLayoutFamily::Specials, + }; + constexpr std::array kCaseStates = { + ImeKbCaseState::Lower, + ImeKbCaseState::Upper, + ImeKbCaseState::CapsLock, + }; + + for (const OrbisImeLanguage language_mask : kLanguageMasks) { + for (const ImeKbLayoutFamily family : kFamilies) { + for (const ImeKbCaseState case_state : kCaseStates) { + for (u8 page = 0; page < 2; ++page) { + const ImeKbLayoutSelection selection{ + .family = family, + .case_state = case_state, + .page = page, + }; + const ImeKbLayoutModel& model = GetImeKeyboardLayout(selection); + AddImeLayoutGlyphs(builder, model, selection, language_mask); + } + } + } + } + + // Done key label is injected dynamically from game params. + AddImeKeyLabelGlyphs(builder, GetEnterLabel(OrbisImeEnterLabel::Default)); + AddImeKeyLabelGlyphs(builder, GetEnterLabel(OrbisImeEnterLabel::Go)); + AddImeKeyLabelGlyphs(builder, GetEnterLabel(OrbisImeEnterLabel::Search)); + AddImeKeyLabelGlyphs(builder, GetEnterLabel(OrbisImeEnterLabel::Send)); + AddImeKeyLabelGlyphs(builder, kArrowLabelUp); + AddImeKeyLabelGlyphs(builder, kArrowLabelRight); + AddImeKeyLabelGlyphs(builder, kArrowLabelDown); + AddImeKeyLabelGlyphs(builder, kArrowLabelLeft); +} + +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; +} + +ImeKbLayoutId ResolveImeKeyboardLayoutId(const ImeKbLayoutSelection& selection) { + switch (selection.family) { + case ImeKbLayoutFamily::Latin: + switch (selection.case_state) { + case ImeKbCaseState::Upper: + return ImeKbLayoutId::LatinUpper; + case ImeKbCaseState::CapsLock: + return ImeKbLayoutId::LatinCapsLock; + case ImeKbCaseState::Lower: + default: + return ImeKbLayoutId::LatinLower; + } + case ImeKbLayoutFamily::Symbols: + return (selection.page % 2 == 0) ? ImeKbLayoutId::SymbolsPage1 + : ImeKbLayoutId::SymbolsPage2; + case ImeKbLayoutFamily::Specials: + return (selection.page % 2 == 0) ? ImeKbLayoutId::SpecialsPage1 + : ImeKbLayoutId::SpecialsPage2; + default: + return ImeKbLayoutId::LatinLower; + } +} + +const ImeKbLayoutModel& GetImeKeyboardLayout(ImeKbLayoutId id) { + switch (id) { + case ImeKbLayoutId::LatinLower: + return kLatinLowerModel; + case ImeKbLayoutId::LatinUpper: + return kLatinUpperModel; + case ImeKbLayoutId::LatinCapsLock: + return kLatinUpperModel; + case ImeKbLayoutId::SymbolsPage1: + return kSymbolsPage1Model; + case ImeKbLayoutId::SymbolsPage2: + return kSymbolsPage2Model; + case ImeKbLayoutId::SpecialsPage1: + return kSpecialsPage1Model; + case ImeKbLayoutId::SpecialsPage2: + return kSpecialsPage2Model; + default: + return GetDefaultLayoutModel(); + } +} + +const ImeKbLayoutModel& GetImeKeyboardLayout(const ImeKbLayoutSelection& selection) { + if (selection.family == ImeKbLayoutFamily::Specials && + selection.case_state != ImeKbCaseState::Lower) { + const bool page1 = (selection.page % 2) == 0; + return page1 ? kSpecialsPage1UpperModel : kSpecialsPage2UpperModel; + } + return GetImeKeyboardLayout(ResolveImeKeyboardLayoutId(selection)); +} + +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig() { + return GetDefaultTopPanelLayoutConfig(); +} + +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig(ImeKbLayoutId id) { + switch (id) { + case ImeKbLayoutId::LatinLower: + case ImeKbLayoutId::LatinUpper: + case ImeKbLayoutId::LatinCapsLock: + case ImeKbLayoutId::SymbolsPage1: + case ImeKbLayoutId::SymbolsPage2: + case ImeKbLayoutId::SpecialsPage1: + case ImeKbLayoutId::SpecialsPage2: + default: + return GetDefaultTopPanelLayoutConfig(); + } +} + +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig(const ImeKbLayoutSelection& selection) { + return GetImeTopPanelLayoutConfig(ResolveImeKeyboardLayoutId(selection)); +} + +ImU32 ImeColorToImU32(const OrbisImeColor& color) { + return IM_COL32(color.r, color.g, color.b, color.a); +} + +ImVec4 ImeColorToImVec4(const OrbisImeColor& color) { + return ImGui::ColorConvertU32ToFloat4(ImeColorToImU32(color)); +} + +ImeStyleConfig GetDefaultImeStyleConfig() { + return ImeStyleConfig{}; +} + +ImeStyleConfig ResolveImeStyleConfig(const OrbisImeParamExtended* extended) { + ImeStyleConfig style = GetDefaultImeStyleConfig(); + if (!extended) { + return style; + } + if (!True(extended->option & OrbisImeExtOption::SET_COLOR)) { + return style; + } + + style.color_base = extended->color_base; + style.color_line = extended->color_line; + style.color_text_field = extended->color_text_field; + style.color_preedit = extended->color_preedit; + style.color_button_default = extended->color_button_default; + style.color_button_function = extended->color_button_function; + style.color_button_symbol = extended->color_button_symbol; + style.color_text = extended->color_text; + style.color_special = extended->color_special; + return style; +} + +void ApplyImeStyleToKeyboardDrawParams(const ImeStyleConfig& style, ImeKbDrawParams& params) { + params.key_bg_default = ImeColorToImU32(style.color_button_default); + params.key_bg_function = ImeColorToImU32(style.color_button_function); + params.key_bg_symbol = ImeColorToImU32(style.color_button_symbol); + params.key_border = ImeColorToImU32(style.color_line); + params.key_done = ImeColorToImU32(style.color_special); + params.key_text = ImeColorToImU32(style.color_text); + + ImVec4 hotkey = ImeColorToImVec4(style.color_text); + hotkey.x = std::clamp(hotkey.x * 0.92f, 0.0f, 1.0f); + hotkey.y = std::clamp(hotkey.y * 0.92f, 0.0f, 1.0f); + hotkey.z = std::clamp(hotkey.z * 0.92f, 0.0f, 1.0f); + params.key_hotkey_text = ImGui::ColorConvertFloat4ToU32(hotkey); +} + +void DrawImeKeyboardGrid(const ImeKbGridLayout& layout, const ImeKbDrawParams& params, + ImeKbDrawState& state) { + state.done_pressed = false; + state.pressed_action = ImeKbKeyAction::None; + state.pressed_label = nullptr; + state.pressed_keycode = 0; + state.pressed_character = u'\0'; + state.selected_row = -1; + state.selected_col = -1; + state.selected_center = {}; + state.hovered = false; + state.clicked = false; + + auto* draw = ImGui::GetWindowDrawList(); + if (!draw || layout.cols <= 0 || layout.rows <= 0 || layout.size.x <= 0.0f || + layout.size.y <= 0.0f) { + return; + } + + const ImeKbLayoutModel* model = params.layout_model; + if (!model) { + model = &GetImeKeyboardLayout(params.selection); + } + if (!model || !model->keys || model->key_count == 0) { + model = &GetDefaultLayoutModel(); + } + + const int grid_cols = layout.cols; + const int grid_rows = layout.rows; + 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 * (grid_cols - 1)) / grid_cols; + const int fixed_bottom_rows = std::clamp(layout.fixed_bottom_rows, 0, grid_rows); + + thread_local std::vector row_heights; + row_heights.assign(static_cast(grid_rows), key_h); + if (fixed_bottom_rows > 0 && layout.bottom_row_h > 0.0f) { + for (int row = grid_rows - fixed_bottom_rows; row < grid_rows; ++row) { + row_heights[static_cast(row)] = layout.bottom_row_h; + } + } + + thread_local std::vector row_offsets; + row_offsets.resize(static_cast(grid_rows)); + float y_cursor = layout.pos.y; + for (int row = 0; row < grid_rows; ++row) { + row_offsets[static_cast(row)] = y_cursor; + y_cursor += row_heights[static_cast(row)]; + if (row + 1 < grid_rows) { + y_cursor += key_gap_y; + } + } + + const auto span_row_height = [&](int row, int span) { + float total = 0.0f; + for (int i = 0; i < span; ++i) { + total += row_heights[static_cast(row + i)]; + } + if (span > 1) { + total += key_gap_y * static_cast(span - 1); + } + return total; + }; + + const auto idx = [grid_cols](int row, int col) { return row * grid_cols + col; }; + thread_local std::vector occupied; + thread_local std::vector is_anchor; + occupied.assign(static_cast(grid_rows * grid_cols), nullptr); + is_anchor.assign(static_cast(grid_rows * grid_cols), false); + + for (std::size_t i = 0; i < model->key_count; ++i) { + const ImeKbKeySpec& key = model->keys[i]; + if (key.col_span == 0 || key.row_span == 0) { + continue; + } + if (key.row >= grid_rows || key.col >= grid_cols) { + continue; + } + + const int row_start = key.row; + const int col_start = key.col; + const int row_end = std::min(grid_rows, static_cast(key.row + key.row_span)); + const int col_end = std::min(grid_cols, static_cast(key.col + key.col_span)); + is_anchor[static_cast(idx(row_start, col_start))] = true; + for (int row = row_start; row < row_end; ++row) { + for (int col = col_start; col < col_end; ++col) { + occupied[static_cast(idx(row, col))] = &key; + } + } + } + + struct RenderedKey { + const ImeKbKeySpec* key = nullptr; + int row = 0; + int col = 0; + u8 col_span = 1; + u8 row_span = 1; + ImVec2 pos{}; + ImVec2 size{}; + ImVec2 center{}; + ImU32 bg = 0; + const char* label = nullptr; + const char* hotkey_label = nullptr; + ImeKbKeyGlyph glyph = ImeKbKeyGlyph::None; + bool underline_label = false; + bool is_done = false; + bool disabled_visual = false; + bool selectable = false; + }; + + thread_local std::vector rendered_keys; + rendered_keys.clear(); + rendered_keys.reserve(static_cast(grid_cols * grid_rows)); + + for (int row = 0; row < grid_rows; ++row) { + for (int col = 0; col < grid_cols; ++col) { + const std::size_t cell_index = static_cast(idx(row, col)); + const ImeKbKeySpec* key = occupied[cell_index]; + if (key && !is_anchor[cell_index]) { + continue; + } + + u8 col_span = 1; + u8 row_span = 1; + if (key) { + col_span = + static_cast(std::min(grid_cols - col, static_cast(key->col_span))); + row_span = + static_cast(std::min(grid_rows - row, static_cast(key->row_span))); + } + + const float x = layout.pos.x + col * (key_w + key_gap_x); + const float y = row_offsets[static_cast(row)]; + const float w = key_w * col_span + key_gap_x * (col_span - 1); + const float h = span_row_height(row, row_span); + ImVec2 pos{x, y}; + ImVec2 size{w, h}; + + const auto slot_base_bg = [&](int slot_row) { + const bool function_row = + fixed_bottom_rows > 0 && slot_row >= std::max(0, grid_rows - fixed_bottom_rows); + if (function_row) { + return params.key_bg_function; + } + if (params.selection.family == ImeKbLayoutFamily::Symbols) { + return params.key_bg_symbol; + } + return params.key_bg_default; + }; + + ImU32 bg = slot_base_bg(row); + bool is_done = false; + const char* label = nullptr; + const char* hotkey_label = nullptr; + ImeKbKeyGlyph glyph = ImeKbKeyGlyph::None; + bool underline_label = false; + bool disabled_visual = false; + if (key) { + const bool symbols_layout = params.selection.family == ImeKbLayoutFamily::Symbols; + const bool typing_key = key->action == ImeKbKeyAction::Character; + const bool function_key = + key->action != ImeKbKeyAction::Character && key->action != ImeKbKeyAction::None; + if (symbols_layout && typing_key) { + bg = params.key_bg_symbol; + } else if (function_key) { + bg = params.key_bg_function; + } else if (key->action == ImeKbKeyAction::None) { + bg = slot_base_bg(row); + } else { + bg = params.key_bg_default; + } + is_done = key->action == ImeKbKeyAction::Done; + if (is_done) { + bg = params.key_done; + label = GetEnterLabel(params.enter_label); + } else { + label = key->label; + } + hotkey_label = key->hotkey_label; + glyph = key->glyph; + + if (!is_done) { + label = ResolveSymbolOverrideLabel(params.selection, params.supported_languages, + *key, label); + label = ResolveShiftOverrideLabel(params.selection, *key, label); + glyph = ResolveShiftOverrideGlyph(params.selection, *key, glyph); + } + } + + rendered_keys.push_back(RenderedKey{ + .key = key, + .row = row, + .col = col, + .col_span = col_span, + .row_span = row_span, + .pos = pos, + .size = size, + .center = {pos.x + size.x * 0.5f, pos.y + size.y * 0.5f}, + .bg = bg, + .label = label, + .hotkey_label = hotkey_label, + .glyph = glyph, + .underline_label = underline_label, + .is_done = is_done, + .disabled_visual = disabled_visual, + .selectable = (key && key->action != ImeKbKeyAction::None), + }); + } + } + + struct ImeKbNavState { + int cursor_row = -1; + int cursor_col = -1; + int fallback_prefer_col_dir = 0; + ImeEdgeWrapNavState edge_wrap_nav{}; + int last_grid_rows = -1; + int last_grid_cols = -1; + int highlighted_row = -1; + int highlighted_col = -1; + int fade_row = -1; + int fade_col = -1; + double fade_started_at = 0.0; + int press_pulse_row = -1; + int press_pulse_col = -1; + double press_pulse_started_at = -1.0; + int mouse_hold_row = -1; + int mouse_hold_col = -1; + bool mouse_hold_prev_down = false; + double mouse_hold_next_repeat_time = 0.0; + }; + static std::unordered_map s_nav_states; + const ImGuiID nav_id = ImGui::GetID("##ImeKbGridNav"); + ImeKbNavState& nav_state = s_nav_states[nav_id]; + const auto clear_mouse_hold = [&]() { + nav_state.mouse_hold_row = -1; + nav_state.mouse_hold_col = -1; + nav_state.mouse_hold_prev_down = false; + nav_state.mouse_hold_next_repeat_time = 0.0; + }; + const auto clear_selection_fade = [&]() { + nav_state.highlighted_row = -1; + nav_state.highlighted_col = -1; + nav_state.fade_row = -1; + nav_state.fade_col = -1; + nav_state.fade_started_at = 0.0; + nav_state.press_pulse_row = -1; + nav_state.press_pulse_col = -1; + nav_state.press_pulse_started_at = -1.0; + }; + const bool grid_shape_changed = + nav_state.last_grid_rows != grid_rows || nav_state.last_grid_cols != grid_cols; + if (grid_shape_changed) { + nav_state.last_grid_rows = grid_rows; + nav_state.last_grid_cols = grid_cols; + ResetImeEdgeWrapNav(nav_state.edge_wrap_nav); + clear_mouse_hold(); + clear_selection_fade(); + } + if (params.reset_nav_state) { + ResetImeEdgeWrapNav(nav_state.edge_wrap_nav); + clear_mouse_hold(); + clear_selection_fade(); + } + + const auto is_selectable_cell = [&](int row, int col) { + if (row < 0 || row >= grid_rows || col < 0 || col >= grid_cols) { + return false; + } + const ImeKbKeySpec* key = occupied[static_cast(idx(row, col))]; + return key && key->action != ImeKbKeyAction::None; + }; + + const auto first_selectable_cell = [&]() -> std::pair { + for (int row = 0; row < grid_rows; ++row) { + for (int col = 0; col < grid_cols; ++col) { + if (is_selectable_cell(row, col)) { + return {row, col}; + } + } + } + return {-1, -1}; + }; + + const auto nearest_selectable_cell = [&](int from_row, int from_col) -> std::pair { + int best_row = -1; + int best_col = -1; + int best_distance = std::numeric_limits::max(); + for (int row = 0; row < grid_rows; ++row) { + for (int col = 0; col < grid_cols; ++col) { + if (!is_selectable_cell(row, col)) { + continue; + } + const int distance = std::abs(row - from_row) + std::abs(col - from_col); + if (distance < best_distance) { + best_distance = distance; + best_row = row; + best_col = col; + } + } + } + return {best_row, best_col}; + }; + + const auto nearest_selectable_cell_on_row = [&](int from_row, int from_col, + int prefer_col_dir) -> std::pair { + if (from_row < 0 || from_row >= grid_rows) { + return {-1, -1}; + } + + int best_col = -1; + int best_distance = std::numeric_limits::max(); + bool best_in_direction = false; + for (int col = 0; col < grid_cols; ++col) { + if (!is_selectable_cell(from_row, col)) { + continue; + } + + const int distance = std::abs(col - from_col); + const bool in_direction = + (prefer_col_dir > 0 && col > from_col) || (prefer_col_dir < 0 && col < from_col); + const bool better_tie = (prefer_col_dir != 0 && in_direction != best_in_direction) + ? in_direction + : col < best_col; + if (distance < best_distance || + (distance == best_distance && (best_col < 0 || better_tie))) { + best_distance = distance; + best_col = col; + best_in_direction = in_direction; + } + } + + return best_col >= 0 ? std::pair{from_row, best_col} + : std::pair{-1, -1}; + }; + + const auto visible_cell_for_cursor = [&]() -> std::pair { + if (is_selectable_cell(nav_state.cursor_row, nav_state.cursor_col)) { + return {nav_state.cursor_row, nav_state.cursor_col}; + } + auto row_fallback = nearest_selectable_cell_on_row( + nav_state.cursor_row, nav_state.cursor_col, nav_state.fallback_prefer_col_dir); + if (row_fallback.first >= 0 && row_fallback.second >= 0) { + return row_fallback; + } + return nearest_selectable_cell(nav_state.cursor_row, nav_state.cursor_col); + }; + + if (params.requested_selected_row >= 0 && params.requested_selected_col >= 0) { + const int row = std::clamp(params.requested_selected_row, 0, std::max(0, grid_rows - 1)); + const int col = std::clamp(params.requested_selected_col, 0, std::max(0, grid_cols - 1)); + auto requested = nearest_selectable_cell_on_row(row, col, 0); + if (requested.first < 0 || requested.second < 0) { + requested = nearest_selectable_cell(row, col); + } + if (requested.first >= 0 && requested.second >= 0) { + nav_state.cursor_row = requested.first; + nav_state.cursor_col = requested.second; + nav_state.fallback_prefer_col_dir = 0; + ResetImeEdgeWrapNav(nav_state.edge_wrap_nav); + } + } + + if (nav_state.cursor_row < 0 || nav_state.cursor_row >= grid_rows || nav_state.cursor_col < 0 || + nav_state.cursor_col >= grid_cols) { + const int anchor_row = std::clamp(nav_state.cursor_row, 0, std::max(0, grid_rows - 1)); + const int anchor_col = std::clamp(nav_state.cursor_col, 0, std::max(0, grid_cols - 1)); + auto [row, col] = nearest_selectable_cell(anchor_row, anchor_col); + if (row < 0 || col < 0) { + const auto first = first_selectable_cell(); + row = first.first; + col = first.second; + } + nav_state.cursor_row = row; + nav_state.cursor_col = col; + nav_state.fallback_prefer_col_dir = 0; + ResetImeEdgeWrapNav(nav_state.edge_wrap_nav); + } + + constexpr double kEdgeWrapHoldDelaySec = 0.5; + constexpr double kRepeatIntentWindowSec = 0.45; + + const auto move_cursor = [&](int step_row, int step_col, bool repeat_hint) { + if (step_row == 0 && step_col == 0) { + return; + } + + int row = nav_state.cursor_row; + int col = nav_state.cursor_col; + const double now = ImGui::GetTime(); + if (row < 0 || col < 0) { + const auto [first_row, first_col] = first_selectable_cell(); + nav_state.cursor_row = first_row; + nav_state.cursor_col = first_col; + nav_state.fallback_prefer_col_dir = 0; + ResetImeEdgeWrapNav(nav_state.edge_wrap_nav); + return; + } + + const ImeKbKeySpec* origin_key = + occupied[static_cast(idx(nav_state.cursor_row, nav_state.cursor_col))]; + if (origin_key && origin_key->action == ImeKbKeyAction::None) { + origin_key = nullptr; + } + const int max_steps = std::max(1, grid_rows * grid_cols); + bool crossed_wrap = false; + for (int i = 0; i < max_steps; ++i) { + crossed_wrap = crossed_wrap || DoesImeKeyboardStepCrossGridEdge( + row, col, step_row, step_col, grid_rows, grid_cols); + const int next_row = row + step_row; + const int next_col = col + step_col; + row = (next_row + grid_rows) % grid_rows; + col = (next_col + grid_cols) % grid_cols; + + const ImeKbKeySpec* candidate_key = occupied[static_cast(idx(row, col))]; + if (!candidate_key || candidate_key->action == ImeKbKeyAction::None) { + continue; + } + // Treat each spanned key as one navigation node in every direction. + if (origin_key && candidate_key == origin_key) { + continue; + } + + if (ShouldDelayImeEdgeWrap(nav_state.edge_wrap_nav, step_row, step_col, repeat_hint, + crossed_wrap, now, kEdgeWrapHoldDelaySec, + kRepeatIntentWindowSec)) { + return; + } + + nav_state.cursor_row = row; + nav_state.cursor_col = col; + nav_state.fallback_prefer_col_dir = step_col; + CommitImeEdgeWrapStep(nav_state.edge_wrap_nav, step_row, step_col, now); + return; + } + }; + + if (params.allow_nav_input) { + const bool use_imgui_lstick_nav = params.use_imgui_lstick_nav; + const bool imgui_move_left_once = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, false) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickLeft, false)); + const bool imgui_move_right_once = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, false) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight, false)); + const bool imgui_move_up_once = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadUp, false) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickUp, false)); + const bool imgui_move_down_once = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadDown, false) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickDown, false)); + const bool imgui_move_left_with_repeat = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickLeft, true)); + const bool imgui_move_right_with_repeat = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight, true)); + const bool imgui_move_up_with_repeat = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadUp, true) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickUp, true)); + const bool imgui_move_down_with_repeat = + ImGui::IsKeyPressed(ImGuiKey_GamepadDpadDown, true) || + (use_imgui_lstick_nav && ImGui::IsKeyPressed(ImGuiKey_GamepadLStickDown, true)); + const bool imgui_move_left_repeat = imgui_move_left_with_repeat && !imgui_move_left_once; + const bool imgui_move_right_repeat = imgui_move_right_with_repeat && !imgui_move_right_once; + const bool imgui_move_up_repeat = imgui_move_up_with_repeat && !imgui_move_up_once; + const bool imgui_move_down_repeat = imgui_move_down_with_repeat && !imgui_move_down_once; + const bool move_left = + imgui_move_left_once || imgui_move_left_repeat || params.external_nav_left; + const bool move_right = + imgui_move_right_once || imgui_move_right_repeat || params.external_nav_right; + const bool move_up = imgui_move_up_once || imgui_move_up_repeat || params.external_nav_up; + const bool move_down = + imgui_move_down_once || imgui_move_down_repeat || params.external_nav_down; + const bool move_left_repeat = + imgui_move_left_repeat || (params.external_nav_left && params.external_nav_left_repeat); + const bool move_right_repeat = + imgui_move_right_repeat || + (params.external_nav_right && params.external_nav_right_repeat); + const bool move_up_repeat = + imgui_move_up_repeat || (params.external_nav_up && params.external_nav_up_repeat); + const bool move_down_repeat = + imgui_move_down_repeat || (params.external_nav_down && params.external_nav_down_repeat); + + if (move_left) { + move_cursor(0, -1, move_left_repeat); + } else if (move_right) { + move_cursor(0, 1, move_right_repeat); + } else if (move_up) { + move_cursor(-1, 0, move_up_repeat); + } else if (move_down) { + move_cursor(1, 0, move_down_repeat); + } + } + + const bool imgui_activate_selected_once = ImGui::IsKeyPressed(ImGuiKey_GamepadFaceDown, false); + const bool imgui_activate_selected_repeat = ImGui::IsKeyPressed(ImGuiKey_GamepadFaceDown, true); + + const auto activate_key = [&](const RenderedKey& render_key, bool trigger_press_pulse) { + if (!render_key.key || !render_key.selectable) { + return; + } + if (trigger_press_pulse) { + nav_state.press_pulse_row = render_key.row; + nav_state.press_pulse_col = render_key.col; + nav_state.press_pulse_started_at = ImGui::GetTime(); + } + state.pressed_action = render_key.key->action; + state.pressed_label = render_key.label; + state.pressed_keycode = ResolveImeKeycode(*render_key.key, render_key.label); + state.pressed_character = ResolveImeCharacter(*render_key.key, render_key.label); + if (render_key.is_done) { + state.done_pressed = true; + } + }; + + int selected_render_index = -1; + const auto [visible_row, visible_col] = visible_cell_for_cursor(); + if (is_selectable_cell(visible_row, visible_col)) { + const ImeKbKeySpec* selected_spec = + occupied[static_cast(idx(visible_row, visible_col))]; + for (int i = 0; i < static_cast(rendered_keys.size()); ++i) { + const auto& key = rendered_keys[static_cast(i)]; + if (key.key == selected_spec && key.selectable) { + selected_render_index = i; + break; + } + } + } + state.selected_row = nav_state.cursor_row; + state.selected_col = nav_state.cursor_col; + if (selected_render_index >= 0) { + state.selected_center = + rendered_keys[static_cast(selected_render_index)].center; + } + + constexpr double kSelectionFadeOutDurationSec = 0.2; + int highlighted_row = -1; + int highlighted_col = -1; + if (params.show_selection_highlight && selected_render_index >= 0) { + const auto& highlighted_key = + rendered_keys[static_cast(selected_render_index)]; + highlighted_row = highlighted_key.row; + highlighted_col = highlighted_key.col; + } + if (highlighted_row != nav_state.highlighted_row || + highlighted_col != nav_state.highlighted_col) { + if (nav_state.highlighted_row >= 0 && nav_state.highlighted_col >= 0) { + nav_state.fade_row = nav_state.highlighted_row; + nav_state.fade_col = nav_state.highlighted_col; + nav_state.fade_started_at = ImGui::GetTime(); + } else { + nav_state.fade_row = -1; + nav_state.fade_col = -1; + nav_state.fade_started_at = 0.0; + } + nav_state.highlighted_row = highlighted_row; + nav_state.highlighted_col = highlighted_col; + } + + float faded_selection_alpha = 0.0f; + if (nav_state.fade_row >= 0 && nav_state.fade_col >= 0 && kSelectionFadeOutDurationSec > 0.0) { + const double elapsed = ImGui::GetTime() - nav_state.fade_started_at; + if (elapsed >= 0.0 && elapsed < kSelectionFadeOutDurationSec) { + faded_selection_alpha = + 1.0f - static_cast(elapsed / kSelectionFadeOutDurationSec); + } else { + nav_state.fade_row = -1; + nav_state.fade_col = -1; + nav_state.fade_started_at = 0.0; + } + } + + const auto action_allows_repeat = [](ImeKbKeyAction action) { + switch (action) { + case ImeKbKeyAction::Character: + case ImeKbKeyAction::Space: + case ImeKbKeyAction::Backspace: + case ImeKbKeyAction::ArrowLeft: + case ImeKbKeyAction::ArrowRight: + case ImeKbKeyAction::ArrowUp: + case ImeKbKeyAction::ArrowDown: + case ImeKbKeyAction::NewLine: + return true; + case ImeKbKeyAction::None: + case ImeKbKeyAction::Shift: + case ImeKbKeyAction::SymbolsMode: + case ImeKbKeyAction::SpecialsMode: + case ImeKbKeyAction::PagePrev: + case ImeKbKeyAction::PageNext: + case ImeKbKeyAction::Keyboard: + case ImeKbKeyAction::Menu: + case ImeKbKeyAction::Settings: + case ImeKbKeyAction::Done: + default: + return false; + } + }; + + bool activate_selected = false; + bool activate_selected_pulse = false; + if (params.allow_activate_input) { + bool activate_once = imgui_activate_selected_once; + bool activate_repeat = imgui_activate_selected_repeat && !imgui_activate_selected_once; + if (!activate_once && params.external_activate_pressed) { + if (params.external_activate_repeat) { + activate_repeat = true; + } else { + activate_once = true; + } + } + + if (selected_render_index >= 0) { + const auto& selected_key = + rendered_keys[static_cast(selected_render_index)]; + const ImeKbKeyAction action = (selected_key.key && selected_key.selectable) + ? selected_key.key->action + : ImeKbKeyAction::None; + if (!action_allows_repeat(action)) { + activate_repeat = false; + } + } + + activate_selected = activate_once || activate_repeat; + activate_selected_pulse = activate_once; + } + + const auto draw_key_glyph = [&](ImVec2 pos, ImVec2 size, ImeKbKeyGlyph glyph) { + if (glyph == ImeKbKeyGlyph::None) { + return; + } + + const float glyph_extent = std::min(size.x, size.y); + const float thickness = std::max(1.2f, glyph_extent * 0.05f); + const ImU32 color = params.key_text; + const float cx = pos.x + size.x * 0.5f; + const float cy = pos.y + size.y * 0.5f; + const auto draw_symbol_glyph = [&](const char* symbol) { + ImFont* font = ImGui::GetFont(); + const float symbol_font_size = std::max(8.0f, glyph_extent * 0.62f); + const ImVec2 symbol_size = font->CalcTextSizeA( + symbol_font_size, std::numeric_limits::max(), -1.0f, symbol); + const ImVec2 symbol_pos{cx - symbol_size.x * 0.5f, cy - symbol_size.y * 0.5f}; + draw->AddText(font, symbol_font_size, symbol_pos, color, symbol); + }; + const auto draw_shift_glyph = [&](bool filled, bool caps_locked) { + const float extent = std::min(size.x, size.y); + const float scale = 0.88f; + const float y_offset = extent * 0.03f; + const float tip_y = cy + y_offset - extent * (0.30f * scale); + const float shoulder_y = cy + y_offset - extent * (0.03f * scale); + const float base_y = cy + y_offset + extent * (0.21f * scale); + const float head_half_w = extent * (0.30f * scale); + const float stem_half_w = extent * (0.11f * scale); + + const std::array shape{ + ImVec2{cx, tip_y}, + ImVec2{cx - head_half_w, shoulder_y}, + ImVec2{cx - stem_half_w, shoulder_y}, + ImVec2{cx - stem_half_w, base_y}, + ImVec2{cx + stem_half_w, base_y}, + ImVec2{cx + stem_half_w, shoulder_y}, + ImVec2{cx + head_half_w, shoulder_y}, + }; + + if (filled) { + draw->AddConvexPolyFilled(shape.data(), static_cast(shape.size()), color); + } else { + draw->AddPolyline(shape.data(), static_cast(shape.size()), color, true, + thickness); + } + + if (caps_locked) { + const float marker_w = extent * (0.24f * scale); + const float marker_h = std::max(thickness * 1.25f, extent * 0.07f); + const float marker_y = + std::min(pos.y + size.y - marker_h * 0.9f, base_y + extent * (0.10f * scale)); + const ImVec2 marker_min{cx - marker_w * 0.5f, marker_y - marker_h * 0.5f}; + const ImVec2 marker_max{cx + marker_w * 0.5f, marker_y + marker_h * 0.5f}; + draw->AddRectFilled(marker_min, marker_max, color, marker_h * 0.45f); + } + }; + + switch (glyph) { + case ImeKbKeyGlyph::Backspace: { + const float extent = std::min(size.x, size.y); + const float body_w = extent * 0.56f; + const float body_h = extent * 0.42f; + const float tip_w = extent * 0.28f; + const float left = cx - (body_w - tip_w) * 0.5f; + const float right = left + body_w; + const float top = cy - body_h * 0.5f; + const float bottom = cy + body_h * 0.5f; + const ImVec2 tip{left - tip_w, cy}; + const std::array frame{ + ImVec2{left, top}, + ImVec2{right, top}, + ImVec2{right, bottom}, + ImVec2{left, bottom}, + tip, + }; + draw->AddPolyline(frame.data(), static_cast(frame.size()), color, true, thickness); + + const float cross_half = extent * 0.10f; + const float cross_cx = left + body_w * 0.57f; + draw->AddLine({cross_cx - cross_half, cy - cross_half}, + {cross_cx + cross_half, cy + cross_half}, color, thickness); + draw->AddLine({cross_cx + cross_half, cy - cross_half}, + {cross_cx - cross_half, cy + cross_half}, color, thickness); + break; + } + case ImeKbKeyGlyph::ArrowLeft: { + draw_symbol_glyph(kArrowLabelLeft); + break; + } + case ImeKbKeyGlyph::ArrowRight: { + draw_symbol_glyph(kArrowLabelRight); + break; + } + case ImeKbKeyGlyph::ArrowUp: { + draw_symbol_glyph(kArrowLabelUp); + break; + } + case ImeKbKeyGlyph::ArrowDown: { + draw_symbol_glyph(kArrowLabelDown); + break; + } + case ImeKbKeyGlyph::ShiftOutline: + draw_shift_glyph(false, false); + break; + case ImeKbKeyGlyph::ShiftFilled: + draw_shift_glyph(true, false); + break; + case ImeKbKeyGlyph::CapsLockFilled: + draw_shift_glyph(true, true); + break; + case ImeKbKeyGlyph::None: + default: + break; + } + }; + + const auto draw_key = [&](ImVec2 pos, ImVec2 size, ImU32 bg, const char* label, + const char* hotkey_label, ImeKbKeyGlyph glyph, float selection_alpha, + float pulse_expand_px, bool emphasize_main_label, + bool underline_main_label, bool disabled_visual, bool done_key) { + const float clamped_selection_alpha = std::clamp(selection_alpha, 0.0f, 1.0f); + if (clamped_selection_alpha > 0.0f) { + const float selection_boost = 0.11f * clamped_selection_alpha; + ImVec4 selected_bg = ImGui::ColorConvertU32ToFloat4(bg); + selected_bg.x = std::min(1.0f, selected_bg.x + selection_boost); + selected_bg.y = std::min(1.0f, selected_bg.y + selection_boost); + selected_bg.z = std::min(1.0f, selected_bg.z + selection_boost); + bg = ImGui::ColorConvertFloat4ToU32(selected_bg); + } + ImU32 key_text_color = params.key_text; + ImU32 key_hotkey_color = params.key_hotkey_text; + if (disabled_visual) { + ImVec4 dim_bg = ImGui::ColorConvertU32ToFloat4(bg); + dim_bg.x *= 0.72f; + dim_bg.y *= 0.72f; + dim_bg.z *= 0.72f; + bg = ImGui::ColorConvertFloat4ToU32(dim_bg); + key_text_color = IM_COL32(150, 150, 150, 255); + key_hotkey_color = IM_COL32(122, 122, 122, 255); + } + const float expand = std::max(0.0f, pulse_expand_px); + const ImVec2 draw_min{pos.x - expand, pos.y - expand}; + const ImVec2 draw_max{pos.x + size.x + expand, pos.y + size.y + expand}; + const float draw_corner_radius = layout.corner_radius + expand; + draw->AddRectFilled(draw_min, draw_max, bg, draw_corner_radius); + const bool selected = clamped_selection_alpha > 0.0f; + ImU32 border_color = params.key_border; + if (done_key) { + ImVec4 done_border = ImGui::ColorConvertU32ToFloat4(bg); + done_border.x = std::clamp(done_border.x * 0.72f, 0.0f, 1.0f); + done_border.y = std::clamp(done_border.y * 0.72f, 0.0f, 1.0f); + done_border.z = std::clamp(done_border.z * 0.72f, 0.0f, 1.0f); + done_border.w = 1.0f; + border_color = ImGui::ColorConvertFloat4ToU32(done_border); + } + if (selected) { + border_color = IM_COL32(248, 248, 248, 255); + } + const float border_thickness = selected ? 2.0f : 1.0f; + if (selected && clamped_selection_alpha < 1.0f) { + ImVec4 border = ImGui::ColorConvertU32ToFloat4(border_color); + border.w *= clamped_selection_alpha; + border_color = ImGui::ColorConvertFloat4ToU32(border); + } + draw->AddRect(draw_min, draw_max, border_color, draw_corner_radius, 0, border_thickness); + ImFont* font = ImGui::GetFont(); + const float base_font_size = ImGui::GetFontSize(); + const auto draw_hotkey_badge = [&]() { + if (!hotkey_label || hotkey_label[0] == '\0') { + return; + } + const float hotkey_padding_x = std::max(3.0f, size.y * 0.08f); + const float hotkey_max_w = std::max(0.0f, size.x - hotkey_padding_x * 2.0f); + float hotkey_font_size = std::max(6.0f, base_font_size * kHotkeyLabelScale); + ImVec2 hotkey_size = font->CalcTextSizeA( + hotkey_font_size, std::numeric_limits::max(), -1.0f, hotkey_label); + if (hotkey_max_w > 0.0f && hotkey_size.x > hotkey_max_w && hotkey_size.x > 0.0f) { + hotkey_font_size *= hotkey_max_w / hotkey_size.x; + hotkey_size = font->CalcTextSizeA( + hotkey_font_size, std::numeric_limits::max(), -1.0f, hotkey_label); + } + ImVec2 hotkey_pos{pos.x + hotkey_padding_x, pos.y}; + const float hotkey_bg_pad_x = std::max(2.0f, hotkey_font_size * 0.28f); + const float hotkey_bg_pad_bottom = std::max(1.0f, hotkey_font_size * 0.16f); + const ImVec2 hotkey_bg_min{hotkey_pos.x - hotkey_bg_pad_x, pos.y}; + const ImVec2 hotkey_bg_max{hotkey_pos.x + hotkey_size.x + hotkey_bg_pad_x, + hotkey_pos.y + hotkey_size.y + hotkey_bg_pad_bottom}; + const float hotkey_bg_rounding = std::max(2.0f, hotkey_font_size * 0.24f); + const ImU32 hotkey_bg_color = + disabled_visual ? IM_COL32(16, 16, 16, 120) : IM_COL32(10, 10, 10, 132); + const ImU32 hotkey_bg_border = + disabled_visual ? IM_COL32(70, 70, 70, 90) : IM_COL32(92, 92, 92, 105); + draw->AddRectFilled(hotkey_bg_min, hotkey_bg_max, hotkey_bg_color, hotkey_bg_rounding); + draw->AddRect(hotkey_bg_min, hotkey_bg_max, hotkey_bg_border, hotkey_bg_rounding, 0, + 1.0f); + draw->AddText(font, hotkey_font_size, hotkey_pos, key_hotkey_color, hotkey_label); + }; + if ((!label || label[0] == '\0') && glyph != ImeKbKeyGlyph::None) { + draw_key_glyph(pos, size, glyph); + } + if (label && label[0] != '\0') { + float label_font_size = + base_font_size * + (emphasize_main_label ? kTypingKeyLabelScale : kFunctionKeyLabelScale); + const float label_padding_x = std::max(4.0f, size.x * 0.08f); + const float label_max_w = std::max(0.0f, size.x - label_padding_x * 2.0f); + ImVec2 text_size = font->CalcTextSizeA(label_font_size, + std::numeric_limits::max(), -1.0f, label); + if (label_max_w > 0.0f && text_size.x > label_max_w && text_size.x > 0.0f) { + label_font_size *= label_max_w / text_size.x; + text_size = font->CalcTextSizeA(label_font_size, std::numeric_limits::max(), + -1.0f, label); + } + ImVec2 text_pos{pos.x + (size.x - text_size.x) * 0.5f, + pos.y + (size.y - text_size.y) * 0.5f}; + draw->AddText(font, label_font_size, text_pos, key_text_color, label); + if (underline_main_label) { + const float underline_pad = std::max(1.0f, label_font_size * 0.05f); + const float underline_y = + std::min(pos.y + size.y - 3.0f, text_pos.y + text_size.y + underline_pad); + const float underline_thickness = std::max(1.0f, label_font_size * 0.06f); + draw->AddLine({text_pos.x, underline_y}, {text_pos.x + text_size.x, underline_y}, + key_text_color, underline_thickness); + } + } + // Draw hotkey tag last so it always stays over main key label/glyph. + draw_hotkey_badge(); + }; + + for (int i = 0; i < static_cast(rendered_keys.size()); ++i) { + const auto& key = rendered_keys[static_cast(i)]; + const bool selected = params.show_selection_highlight && (i == selected_render_index); + float selection_alpha = selected ? 1.0f : 0.0f; + if (!selected && faded_selection_alpha > 0.0f && key.row == nav_state.fade_row && + key.col == nav_state.fade_col) { + selection_alpha = faded_selection_alpha; + } + constexpr float kSelectedBorderThickness = 2.0f; + float press_pulse_expand = 0.0f; + if (selected && key.row == nav_state.press_pulse_row && + key.col == nav_state.press_pulse_col) { + press_pulse_expand = ComputePressPulseExpand( + nav_state.press_pulse_started_at, ImGui::GetTime(), kSelectorPressPulseDurationSec, + kSelectedBorderThickness * kSelectorPressPulseExpandBorderFactor); + } + const bool emphasize_main_label = key.key && key.key->action == ImeKbKeyAction::Character; + const bool underline_main_label = key.underline_label; + draw_key(key.pos, key.size, key.bg, key.label, key.hotkey_label, key.glyph, selection_alpha, + press_pulse_expand, emphasize_main_label, underline_main_label, + key.disabled_visual, key.is_done); + + if (!key.selectable) { + continue; + } + + ImGui::PushID(idx(key.row, key.col)); + ImGui::SetCursorScreenPos(key.pos); + ImGui::PushItemFlag(ImGuiItemFlags_NoNav, true); + (void)ImGui::InvisibleButton("##ImeGridKey", key.size); + ImGui::PopItemFlag(); + if (ImGui::IsItemHovered()) { + state.hovered = true; + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + state.clicked = true; + nav_state.cursor_row = key.row; + nav_state.cursor_col = key.col; + nav_state.fallback_prefer_col_dir = 0; + activate_key(key, true); + if (key.key && action_allows_repeat(key.key->action)) { + nav_state.mouse_hold_row = key.row; + nav_state.mouse_hold_col = key.col; + nav_state.mouse_hold_prev_down = true; + nav_state.mouse_hold_next_repeat_time = + ImGui::GetTime() + static_cast(ImGui::GetIO().KeyRepeatDelay); + } else { + clear_mouse_hold(); + } + } + ImGui::PopID(); + } + + if (nav_state.mouse_hold_row >= 0 && nav_state.mouse_hold_col >= 0) { + int hold_render_index = -1; + for (int i = 0; i < static_cast(rendered_keys.size()); ++i) { + const auto& key = rendered_keys[static_cast(i)]; + if (!key.selectable) { + continue; + } + if (key.row == nav_state.mouse_hold_row && key.col == nav_state.mouse_hold_col) { + hold_render_index = i; + break; + } + } + + if (hold_render_index < 0 || !ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + clear_mouse_hold(); + } else { + const auto& hold_key = rendered_keys[static_cast(hold_render_index)]; + const ImeKbKeyAction hold_action = + hold_key.key ? hold_key.key->action : ImeKbKeyAction::None; + if (!action_allows_repeat(hold_action)) { + clear_mouse_hold(); + } else { + const ImVec2 mouse_pos = ImGui::GetIO().MousePos; + const bool mouse_over_hold_key = + mouse_pos.x >= hold_key.pos.x && + mouse_pos.x <= (hold_key.pos.x + hold_key.size.x) && + mouse_pos.y >= hold_key.pos.y && + mouse_pos.y <= (hold_key.pos.y + hold_key.size.y); + const bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left) && mouse_over_hold_key; + const double now = ImGui::GetTime(); + const double repeat_delay = static_cast(ImGui::GetIO().KeyRepeatDelay); + const double repeat_rate = static_cast(ImGui::GetIO().KeyRepeatRate); + bool trigger_repeat = false; + if (!down) { + nav_state.mouse_hold_prev_down = false; + nav_state.mouse_hold_next_repeat_time = 0.0; + } else if (!nav_state.mouse_hold_prev_down) { + nav_state.mouse_hold_prev_down = true; + nav_state.mouse_hold_next_repeat_time = now + repeat_delay; + } else if (repeat_rate > 0.0 && now >= nav_state.mouse_hold_next_repeat_time) { + nav_state.mouse_hold_next_repeat_time = now + repeat_rate; + trigger_repeat = true; + } + + if (trigger_repeat) { + activate_key(hold_key, false); + } + } + } + } + + if (activate_selected && selected_render_index >= 0) { + activate_key(rendered_keys[static_cast(selected_render_index)], + activate_selected_pulse); + } +} + +} // 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 000000000..b1c7cd1b5 --- /dev/null +++ b/src/core/libraries/ime/ime_kb_layout.h @@ -0,0 +1,561 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#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; + // Base row height used for non-fixed rows. + float key_h = 0.0f; + // Optional fixed-size bottom rows (used by accents/specials to preserve function-row height). + float bottom_row_h = 0.0f; + int fixed_bottom_rows = 0; + int cols = 10; + int rows = 6; + float corner_radius = 0.0f; +}; + +// Standardized OSK selection indexing: +// - Keyboard grid uses zero-based row/column indexing. +// - Top panel row placement is driven by ImeTopPanelLayoutConfig::row/row_span. +// - Keyboard rows follow active ImeKbLayoutModel::rows. +// - Function row count is driven by ImeKbLayoutModel::function_rows. +struct ImeSelectionGridIndex { + static constexpr int DefaultKeyboardRows = 6; + static constexpr int DefaultKeyboardCols = 10; + static constexpr int DefaultTopPanelRow = 0; + static constexpr int DefaultTopPanelRows = 1; + static constexpr int DefaultFunctionRows = 2; + + static constexpr int PanelMinRow = 0; + static constexpr int PanelMaxRow = 6; // Legacy default for 6-row keyboard layouts. + static constexpr int PanelTopRow = 0; + static constexpr int PanelKeyboardMinRow = 1; + static constexpr int PanelKeyboardMaxRow = 6; // Legacy default for 6-row keyboard layouts. + + static constexpr int KeyboardMinRow = 0; + static constexpr int KeyboardMaxRow = 5; // Legacy default for 6-row keyboard layouts. + static constexpr int KeyboardMinCol = 0; + static constexpr int KeyboardMaxCol = 9; // Legacy default for 10-column keyboard layouts. + + static constexpr int TopRowMinCol = 0; + static constexpr int TopRowPredictionMaxCol = 8; + static constexpr int TopRowCloseCol = 9; + + static constexpr int PanelTopRowFromConfig(int top_panel_row = DefaultTopPanelRow) { + return std::max(PanelMinRow, top_panel_row); + } + + static constexpr int PanelTopRowsFromConfig(int top_panel_rows = DefaultTopPanelRows) { + return std::max(1, top_panel_rows); + } + + static constexpr int KeyboardMaxRowForRows(int keyboard_rows) { + return std::max(0, keyboard_rows - 1); + } + + static constexpr int KeyboardMaxColForCols(int keyboard_cols) { + return std::max(0, keyboard_cols - 1); + } + + static constexpr int PanelKeyboardMinRowForTopPanel(int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return PanelTopRowFromConfig(top_panel_row) + PanelTopRowsFromConfig(top_panel_rows); + } + + static constexpr int PanelKeyboardMaxRowForKeyboardRows( + int keyboard_rows, int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return PanelKeyboardMinRowForTopPanel(top_panel_row, top_panel_rows) + + KeyboardMaxRowForRows(std::max(1, keyboard_rows)); + } + + static constexpr int PanelMaxRowForKeyboardRows(int keyboard_rows, + int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return PanelKeyboardMaxRowForKeyboardRows(keyboard_rows, top_panel_row, top_panel_rows); + } + + static constexpr int ResolveFunctionRows(int keyboard_rows, + int configured_function_rows = DefaultFunctionRows) { + return keyboard_rows > 2 + ? std::min(std::max(0, configured_function_rows), keyboard_rows - 1) + : 0; + } + + static constexpr int ResolveTypingRows(int keyboard_rows, + int configured_function_rows = DefaultFunctionRows) { + const int rows = std::max(1, keyboard_rows); + return std::max(1, rows - ResolveFunctionRows(rows, configured_function_rows)); + } + + static constexpr int KeyboardFunctionMinRow( + int keyboard_rows, int configured_function_rows = DefaultFunctionRows) { + const int rows = std::max(1, keyboard_rows); + const int function_rows = ResolveFunctionRows(rows, configured_function_rows); + return function_rows > 0 ? (rows - function_rows) : rows; + } + + static constexpr bool IsKeyboardFunctionRow( + int keyboard_row, int keyboard_rows, int configured_function_rows = DefaultFunctionRows) { + const int rows = std::max(1, keyboard_rows); + return keyboard_row >= KeyboardFunctionMinRow(rows, configured_function_rows) && + keyboard_row <= KeyboardMaxRowForRows(rows); + } + + static constexpr int ClampPanelRow(int row, int keyboard_rows = DefaultKeyboardRows, + int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return std::clamp(row, PanelMinRow, + PanelMaxRowForKeyboardRows(keyboard_rows, top_panel_row, top_panel_rows)); + } + + static constexpr int ClampKeyboardRow(int row, int keyboard_rows = DefaultKeyboardRows) { + return std::clamp(row, KeyboardMinRow, KeyboardMaxRowForRows(keyboard_rows)); + } + + static constexpr int ClampKeyboardCol(int col, int keyboard_cols = DefaultKeyboardCols) { + return std::clamp(col, KeyboardMinCol, KeyboardMaxColForCols(keyboard_cols)); + } + + static constexpr int ClampTopRowCol(int col) { + return std::clamp(col, TopRowMinCol, TopRowCloseCol); + } + + static constexpr int ClampTopPredictionCol(int col) { + return std::clamp(col, TopRowMinCol, TopRowPredictionMaxCol); + } + + static constexpr int TopToKeyboardCol(int top_col, int keyboard_cols = DefaultKeyboardCols) { + return ClampKeyboardCol(top_col, keyboard_cols); + } + + static constexpr int KeyboardToTopCol(int keyboard_col) { + return ClampTopRowCol(keyboard_col); + } + + static constexpr int PanelToKeyboardRow(int panel_row, int keyboard_rows = DefaultKeyboardRows, + int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return ClampKeyboardRow(panel_row - + PanelKeyboardMinRowForTopPanel(top_panel_row, top_panel_rows), + keyboard_rows); + } + + static constexpr int KeyboardToPanelRow(int keyboard_row, + int keyboard_rows = DefaultKeyboardRows, + int top_panel_row = DefaultTopPanelRow, + int top_panel_rows = DefaultTopPanelRows) { + return ClampPanelRow(keyboard_row + + PanelKeyboardMinRowForTopPanel(top_panel_row, top_panel_rows), + keyboard_rows, top_panel_row, top_panel_rows); + } + + static int GridColumnFromX(float x, float left, float width, int min_col, int max_col) { + if (max_col < min_col || width <= 0.0f) { + return min_col; + } + const float t = std::clamp((x - left) / width, 0.0f, 0.9999f); + const int span = max_col - min_col + 1; + const int offset = std::clamp(static_cast(t * static_cast(span)), 0, span - 1); + return min_col + offset; + } +}; + +enum class ImeKbLayoutFamily : u8 { + Latin = 0, + Symbols = 1, + Specials = 2, +}; + +enum class ImeKbCaseState : u8 { + Lower = 0, + Upper = 1, + CapsLock = 2, +}; + +enum class ImeKbLayoutId : u8 { + LatinLower = 0, + LatinUpper = 1, + LatinCapsLock = 2, + SymbolsPage1 = 3, + SymbolsPage2 = 4, + SpecialsPage1 = 5, + SpecialsPage2 = 6, +}; + +struct ImeKbLayoutSelection { + ImeKbLayoutFamily family = ImeKbLayoutFamily::Latin; + ImeKbCaseState case_state = ImeKbCaseState::Lower; + u8 page = 0; +}; + +enum class ImeKbKeyAction : u8 { + // Disabled key slot. It is not selectable and should not expose any visible label. + None = 0, + Character = 1, + Shift = 2, + SymbolsMode = 3, + SpecialsMode = 4, + Space = 5, + Backspace = 6, + ArrowLeft = 7, + ArrowRight = 8, + ArrowUp = 9, + ArrowDown = 10, + Keyboard = 11, + Menu = 12, + Settings = 13, + NewLine = 14, + Done = 15, + PagePrev = 16, + PageNext = 17, +}; + +enum class ImeKbKeyGlyph : u8 { + None = 0, + Backspace = 1, + ArrowLeft = 2, + ArrowRight = 3, + ArrowUp = 4, + ArrowDown = 5, + ShiftOutline = 6, + ShiftFilled = 7, + CapsLockFilled = 8, +}; + +struct ImeKbKeySpec { + u8 row = 0; + u8 col = 0; + u8 col_span = 1; + u8 row_span = 1; + // Disabled key contract: + // - action == ImeKbKeyAction::None + // - label == nullptr + // - hotkey_label == nullptr + // - glyph == ImeKbKeyGlyph::None + // This means the key loses both label visibility and functionality. + const char* label = nullptr; + const char* hotkey_label = nullptr; + ImeKbKeyAction action = ImeKbKeyAction::None; + ImeKbKeyGlyph glyph = ImeKbKeyGlyph::None; +}; + +struct ImeKbLayoutModel { + const ImeKbKeySpec* keys = nullptr; + std::size_t key_count = 0; + u8 cols = 10; + u8 rows = 6; + u8 function_rows = static_cast(ImeSelectionGridIndex::DefaultFunctionRows); +}; + +enum class ImeTopPanelElementId : u8 { + Prediction = 0, + Close = 1, +}; + +struct ImeTopPanelElementSpec { + ImeTopPanelElementId id = ImeTopPanelElementId::Prediction; + u8 col = 0; + u8 col_span = 1; +}; + +struct ImeTopPanelLayoutConfig { + const ImeTopPanelElementSpec* elements = nullptr; + std::size_t element_count = 0; + u8 cols = 10; + u8 row = static_cast(ImeSelectionGridIndex::DefaultTopPanelRow); + u8 row_span = static_cast(ImeSelectionGridIndex::DefaultTopPanelRows); +}; + +// Mirrors OrbisImeParamExtended color buckets so UI styling can be themed +// through one config and optionally overridden by game-provided SET_COLOR. +struct ImeStyleConfig { + OrbisImeColor color_base{18, 18, 18, 255}; + OrbisImeColor color_line{70, 70, 70, 255}; + OrbisImeColor color_text_field{22, 37, 60, 255}; + OrbisImeColor color_preedit{35, 35, 35, 255}; + OrbisImeColor color_button_default{35, 35, 35, 255}; + OrbisImeColor color_button_function{60, 60, 60, 255}; + OrbisImeColor color_button_symbol{78, 78, 78, 255}; + OrbisImeColor color_text{230, 230, 230, 255}; + OrbisImeColor color_special{30, 90, 170, 255}; +}; + +// Shared edge-wrap hold state for controller navigation. +// It tracks last successful move direction/time and active wrap hold window. +struct ImeEdgeWrapNavState { + int last_step_row = 0; + int last_step_col = 0; + double last_step_time = -1.0; + int hold_step_row = 0; + int hold_step_col = 0; + double hold_release_time = 0.0; + bool hold_active = false; +}; + +inline void ResetImeEdgeWrapHold(ImeEdgeWrapNavState& state) { + state.hold_step_row = 0; + state.hold_step_col = 0; + state.hold_release_time = 0.0; + state.hold_active = false; +} + +inline void ResetImeEdgeWrapNav(ImeEdgeWrapNavState& state) { + state.last_step_row = 0; + state.last_step_col = 0; + state.last_step_time = -1.0; + ResetImeEdgeWrapHold(state); +} + +inline bool ShouldDelayImeEdgeWrap(ImeEdgeWrapNavState& state, int step_row, int step_col, + bool repeat_hint, bool wraps, double now, double hold_delay_sec, + double repeat_window_sec) { + (void)repeat_window_sec; + if (step_row == 0 && step_col == 0) { + return false; + } + if (state.hold_active && (state.hold_step_row != step_row || state.hold_step_col != step_col)) { + ResetImeEdgeWrapHold(state); + } + + if (!wraps) { + return false; + } + + const bool same_wrap_direction = + state.hold_active && state.hold_step_row == step_row && state.hold_step_col == step_col; + if (!(repeat_hint || same_wrap_direction)) { + return false; + } + + if (!same_wrap_direction) { + state.hold_step_row = step_row; + state.hold_step_col = step_col; + state.hold_release_time = now + hold_delay_sec; + state.hold_active = true; + } + return now < state.hold_release_time; +} + +inline void CommitImeEdgeWrapStep(ImeEdgeWrapNavState& state, int step_row, int step_col, + double now) { + state.last_step_row = step_row; + state.last_step_col = step_col; + state.last_step_time = now; + ResetImeEdgeWrapHold(state); +} + +inline const ImeKbKeySpec* ResolveImeKeyboardKeyAt(const ImeKbLayoutModel& layout, int row, + int col) { + const int grid_cols = std::max(1, static_cast(layout.cols)); + const int grid_rows = std::max(1, static_cast(layout.rows)); + if (row < 0 || row >= grid_rows || col < 0 || col >= grid_cols) { + return nullptr; + } + if (!layout.keys || layout.key_count == 0) { + return nullptr; + } + + const ImeKbKeySpec* resolved = nullptr; + for (std::size_t i = 0; i < layout.key_count; ++i) { + const auto& key = layout.keys[i]; + if (key.col_span == 0 || key.row_span == 0) { + continue; + } + if (key.row >= grid_rows || key.col >= grid_cols) { + continue; + } + const int row_start = static_cast(key.row); + const int col_start = static_cast(key.col); + const int row_span = std::max(1, static_cast(key.row_span)); + const int col_span = std::max(1, static_cast(key.col_span)); + const int row_end = std::min(grid_rows, row_start + row_span); + const int col_end = std::min(grid_cols, col_start + col_span); + if (row >= row_start && row < row_end && col >= col_start && col < col_end) { + // Match DrawImeKeyboardGrid occupancy behavior: later keys override earlier ones. + resolved = &key; + } + } + return resolved; +} + +inline bool DoesImeKeyboardStepCrossGridEdge(int from_row, int from_col, int step_row, int step_col, + int grid_rows, int grid_cols) { + if (step_row == 0 && step_col == 0) { + return false; + } + if (from_row < 0 || from_row >= grid_rows || from_col < 0 || from_col >= grid_cols) { + return false; + } + + const int next_row = from_row + step_row; + const int next_col = from_col + step_col; + return next_row < 0 || next_row >= grid_rows || next_col < 0 || next_col >= grid_cols; +} + +inline bool DoesImeKeyboardNavigationWrap(const ImeKbLayoutModel& layout, int from_row, + int from_col, int step_row, int step_col) { + if (step_row == 0 && step_col == 0) { + return false; + } + + const int grid_cols = std::max(1, static_cast(layout.cols)); + const int grid_rows = std::max(1, static_cast(layout.rows)); + if (from_row < 0 || from_row >= grid_rows || from_col < 0 || from_col >= grid_cols) { + return false; + } + + const auto* origin_key = ResolveImeKeyboardKeyAt(layout, from_row, from_col); + if (origin_key && origin_key->action == ImeKbKeyAction::None) { + origin_key = nullptr; + } + + int row = from_row; + int col = from_col; + bool crossed_wrap = false; + const int max_steps = std::max(1, grid_rows * grid_cols); + for (int i = 0; i < max_steps; ++i) { + crossed_wrap = crossed_wrap || DoesImeKeyboardStepCrossGridEdge( + row, col, step_row, step_col, grid_rows, grid_cols); + + const int next_row = row + step_row; + const int next_col = col + step_col; + row = (next_row + grid_rows) % grid_rows; + col = (next_col + grid_cols) % grid_cols; + + const auto* candidate_key = ResolveImeKeyboardKeyAt(layout, row, col); + if (!candidate_key || candidate_key->action == ImeKbKeyAction::None) { + continue; + } + if (origin_key && candidate_key == origin_key) { + continue; + } + return crossed_wrap; + } + return false; +} + +struct ImeKbDrawParams { + ImeKbLayoutSelection selection{}; + const ImeKbLayoutModel* layout_model = nullptr; + OrbisImeLanguage supported_languages = static_cast(0); + OrbisImeEnterLabel enter_label = OrbisImeEnterLabel::Default; + bool show_selection_highlight = true; + bool allow_nav_input = true; + bool use_imgui_lstick_nav = true; + bool allow_activate_input = true; + bool external_nav_left = false; + bool external_nav_right = false; + bool external_nav_up = false; + bool external_nav_down = false; + bool external_nav_left_repeat = false; + bool external_nav_right_repeat = false; + bool external_nav_up_repeat = false; + bool external_nav_down_repeat = false; + bool external_activate_pressed = false; + bool external_activate_repeat = false; + bool reset_nav_state = false; + int requested_selected_row = -1; + int requested_selected_col = -1; + ImU32 key_bg_default = IM_COL32(35, 35, 35, 255); + ImU32 key_bg_function = IM_COL32(60, 60, 60, 255); + ImU32 key_bg_symbol = IM_COL32(78, 78, 78, 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); + ImU32 key_hotkey_text = IM_COL32(220, 220, 220, 255); +}; + +struct ImeKbDrawState { + bool done_pressed = false; + ImeKbKeyAction pressed_action = ImeKbKeyAction::None; + const char* pressed_label = nullptr; + u16 pressed_keycode = 0; + char16_t pressed_character = u'\0'; + // Logical grid cursor. If the cursor cell is disabled, selected_center points to the + // visible nearest enabled fallback key used for drawing and activation. + int selected_row = -1; + int selected_col = -1; + ImVec2 selected_center{}; + bool hovered = false; + bool clicked = 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); +ImeKbLayoutId ResolveImeKeyboardLayoutId(const ImeKbLayoutSelection& selection); +const ImeKbLayoutModel& GetImeKeyboardLayout(ImeKbLayoutId id); +const ImeKbLayoutModel& GetImeKeyboardLayout(const ImeKbLayoutSelection& selection); +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig(); +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig(ImeKbLayoutId id); +const ImeTopPanelLayoutConfig& GetImeTopPanelLayoutConfig(const ImeKbLayoutSelection& selection); +ImeStyleConfig GetDefaultImeStyleConfig(); +ImeStyleConfig ResolveImeStyleConfig(const OrbisImeParamExtended* extended); +ImU32 ImeColorToImU32(const OrbisImeColor& color); +ImVec4 ImeColorToImVec4(const OrbisImeColor& color); +void ApplyImeStyleToKeyboardDrawParams(const ImeStyleConfig& style, ImeKbDrawParams& params); +void AddImeKeyboardGlyphsToFontRanges(ImFontGlyphRangesBuilder& builder); + +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 19b9e2e8b..a0d4ff9d9 100644 --- a/src/core/libraries/ime/ime_ui.cpp +++ b/src/core/libraries/ime/ime_ui.cpp @@ -2,15 +2,36 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include "core/libraries/ime/ime_kb_layout.h" +#include "core/libraries/ime/ime_ui_shared.h" +#include "core/libraries/pad/pad.h" #include "ime_ui.h" #include "imgui/imgui_std.h" +#include "imgui/renderer/imgui_core.h" namespace Libraries::Ime { using namespace ImGui; +namespace { -static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f}; +constexpr ImU32 kSelectorBorderColor = IM_COL32(248, 248, 248, 255); +constexpr ImU32 kSelectorOverlayColor = IM_COL32(255, 255, 255, 18); +constexpr float kSelectorBorderThickness = 2.0f; +constexpr float kSelectorInnerMargin = 2.0f; +constexpr const char* kSelectorInputId = "##ImeSelectorInput"; +constexpr const char* kSelectorPredictId = "##ImeSelectorPredict"; +constexpr const char* kSelectorCloseId = "##ImeSelectorClose"; + +} // namespace ImeState::ImeState(const OrbisImeParam* param, const OrbisImeParamExtended* extended) { if (!param) { @@ -41,14 +62,23 @@ ImeState::ImeState(const OrbisImeParam* param, const OrbisImeParamExtended* exte LOG_ERROR(Lib_Ime, "Failed to convert text to utf8 encoding"); } } + const int current_len_utf16 = Utf16CountFromUtf8Range( + current_text.begin(), current_text.begin() + static_cast(current_text.size())); + caret_index = current_len_utf16; + caret_byte_index = Utf8ByteIndexFromUtf16Index(current_text.begin(), caret_index); + caret_dirty = true; } ImeState::ImeState(ImeState&& other) noexcept : work_buffer(other.work_buffer), text_buffer(other.text_buffer), max_text_length(other.max_text_length), current_text(std::move(other.current_text)), - event_queue(std::move(other.event_queue)) { + caret_index(other.caret_index), caret_byte_index(other.caret_byte_index), + caret_dirty(other.caret_dirty), event_queue(std::move(other.event_queue)) { other.text_buffer = nullptr; other.max_text_length = 0; + other.caret_index = 0; + other.caret_byte_index = 0; + other.caret_dirty = false; } ImeState& ImeState::operator=(ImeState&& other) noexcept { @@ -57,10 +87,16 @@ ImeState& ImeState::operator=(ImeState&& other) noexcept { text_buffer = other.text_buffer; max_text_length = other.max_text_length; current_text = std::move(other.current_text); + caret_index = other.caret_index; + caret_byte_index = other.caret_byte_index; + caret_dirty = other.caret_dirty; event_queue = std::move(other.event_queue); other.text_buffer = nullptr; other.max_text_length = 0; + other.caret_index = 0; + other.caret_byte_index = 0; + other.caret_dirty = false; } return *this; } @@ -88,12 +124,12 @@ void ImeState::SendEnterEvent() { } if (text.str) { const u32 len = static_cast(std::char_traits::length(text.str)); - // 0-based caret at end - text.caret_index = len; + const u32 caret = static_cast(std::clamp(caret_index, 0, static_cast(len))); + text.caret_index = caret; text.area_num = 1; text.text_area[0].mode = OrbisImeTextAreaMode::Edit; // No edit happening on Enter: length=0; index can be caret - text.text_area[0].index = len; + text.text_area[0].index = caret; text.text_area[0].length = 0; enterEvent.param.text = text; } @@ -122,12 +158,12 @@ void ImeState::SendCloseEvent() { } if (text.str) { const u32 len = static_cast(std::char_traits::length(text.str)); - // 0-based caret at end - text.caret_index = len; + const u32 caret = static_cast(std::clamp(caret_index, 0, static_cast(len))); + text.caret_index = caret; text.area_num = 1; text.text_area[0].mode = OrbisImeTextAreaMode::Edit; // No edit happening on Close: length=0; index can be caret - text.text_area[0].index = len; + text.text_area[0].index = caret; text.text_area[0].length = 0; closeEvent.param.text = text; } @@ -150,8 +186,28 @@ void ImeState::SetText(const char16_t* text, u32 length) { LOG_ERROR(Lib_Ime, "ImeState::SetText failed to convert updated text to UTF-8"); return; } + + const int len_utf16 = Utf16CountFromUtf8Range( + current_text.begin(), current_text.begin() + static_cast(current_text.size())); + if (caret_index < 0) { + caret_index = 0; + } else if (caret_index > len_utf16) { + caret_index = len_utf16; + } + caret_byte_index = Utf8ByteIndexFromUtf16Index(current_text.begin(), caret_index); + caret_dirty = true; +} + +void ImeState::SetCaret(u32 position) { + const int len_utf16 = Utf16CountFromUtf8Range( + current_text.begin(), current_text.begin() + static_cast(current_text.size())); + const int next_caret = std::clamp(static_cast(position), 0, len_utf16); + caret_index = next_caret; + caret_byte_index = Utf8ByteIndexFromUtf16Index(current_text.begin(), next_caret); + caret_dirty = true; + LOG_DEBUG(Lib_Ime, "ImeState::SetCaret requested={}, applied={} (len={})", position, + caret_index, len_utf16); } -void ImeState::SetCaret(u32 position) {} bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len, char* utf8_text, std::size_t utf8_text_len) { @@ -172,9 +228,25 @@ bool ImeState::ConvertUTF8ToOrbis(const char* utf8_text, std::size_t utf8_text_l } ImeUi::ImeUi(ImeState* state, const OrbisImeParam* param, const OrbisImeParamExtended* extended) - : state(state), ime_param(param), extended_param(extended) { + : state(state), ime_param(param), extended_param(extended), + style_config(ResolveImeStyleConfig(extended)) { if (param) { + const OrbisImeExtOption ext_option = + extended_param ? extended_param->option : OrbisImeExtOption::DEFAULT; + const OrbisImePanelPriority panel_priority = + extended_param ? extended_param->priority : OrbisImePanelPriority::Default; + kb_layout_selection = ResolveInitialKbLayoutSelection(ext_option, panel_priority); + last_nav_layout_selection = kb_layout_selection; + nav_layout_selection_initialized = true; + kb_alpha_family = (kb_layout_selection.family == ImeKbLayoutFamily::Specials) + ? ImeKbLayoutFamily::Specials + : ImeKbLayoutFamily::Latin; + InitializeDefaultOskSelectionAnchor(kb_layout_selection, ext_option, pending_keyboard_row, + pending_keyboard_col, last_keyboard_selected_row, + last_keyboard_selected_col); AddLayer(this); + ImGui::Core::AcquireGamepadInputCapture(); + gamepad_input_capture_active = true; } } @@ -189,11 +261,104 @@ ImeUi& ImeUi::operator=(ImeUi&& other) { state = other.state; ime_param = other.ime_param; + extended_param = other.extended_param; + style_config = other.style_config; first_render = other.first_render; + accept_armed = other.accept_armed; + native_input_active = other.native_input_active; + pointer_navigation_active = other.pointer_navigation_active; + edit_menu_popup = other.edit_menu_popup; + menu_activate_armed = other.menu_activate_armed; + l2_shortcut_armed = other.l2_shortcut_armed; + request_input_focus = other.request_input_focus; + request_input_select_all = other.request_input_select_all; + text_select_mode = other.text_select_mode; + pending_input_selection_apply = other.pending_input_selection_apply; + prev_virtual_cross_down = other.prev_virtual_cross_down; + prev_virtual_lstick_left_down = other.prev_virtual_lstick_left_down; + prev_virtual_lstick_right_down = other.prev_virtual_lstick_right_down; + prev_virtual_lstick_up_down = other.prev_virtual_lstick_up_down; + prev_virtual_lstick_down_down = other.prev_virtual_lstick_down_down; + left_stick_repeat_dir = other.left_stick_repeat_dir; + left_stick_next_repeat_time = other.left_stick_next_repeat_time; + virtual_cross_next_repeat_time = other.virtual_cross_next_repeat_time; + virtual_triangle_next_repeat_time = other.virtual_triangle_next_repeat_time; + prev_virtual_buttons = other.prev_virtual_buttons; + prev_virtual_square_down = other.prev_virtual_square_down; + prev_virtual_l1_down = other.prev_virtual_l1_down; + prev_virtual_r1_down = other.prev_virtual_r1_down; + prev_virtual_dpad_left_down = other.prev_virtual_dpad_left_down; + prev_virtual_dpad_right_down = other.prev_virtual_dpad_right_down; + prev_virtual_dpad_up_down = other.prev_virtual_dpad_up_down; + prev_virtual_dpad_down_down = other.prev_virtual_dpad_down_down; + virtual_square_next_repeat_time = other.virtual_square_next_repeat_time; + virtual_l1_next_repeat_time = other.virtual_l1_next_repeat_time; + virtual_r1_next_repeat_time = other.virtual_r1_next_repeat_time; + virtual_dpad_left_next_repeat_time = other.virtual_dpad_left_next_repeat_time; + virtual_dpad_right_next_repeat_time = other.virtual_dpad_right_next_repeat_time; + virtual_dpad_up_next_repeat_time = other.virtual_dpad_up_next_repeat_time; + virtual_dpad_down_next_repeat_time = other.virtual_dpad_down_next_repeat_time; + panel_vertical_nav_state = other.panel_vertical_nav_state; + panel_position_initialized = other.panel_position_initialized; + panel_drag_active = other.panel_drag_active; + panel_position = other.panel_position; + input_cursor_utf16 = other.input_cursor_utf16; + input_cursor_byte = other.input_cursor_byte; + input_selection_start_byte = other.input_selection_start_byte; + input_selection_end_byte = other.input_selection_end_byte; + text_select_anchor_utf16 = other.text_select_anchor_utf16; + text_select_focus_utf16 = other.text_select_focus_utf16; + top_virtual_col = other.top_virtual_col; + panel_selection = other.panel_selection; + pending_keyboard_row = other.pending_keyboard_row; + pending_keyboard_col = other.pending_keyboard_col; + last_keyboard_selected_row = other.last_keyboard_selected_row; + last_keyboard_selected_col = other.last_keyboard_selected_col; + edit_menu_index = other.edit_menu_index; + kb_layout_selection = other.kb_layout_selection; + last_nav_layout_selection = other.last_nav_layout_selection; + nav_layout_selection_initialized = other.nav_layout_selection_initialized; + kb_alpha_family = other.kb_alpha_family; + gamepad_input_capture_active = other.gamepad_input_capture_active; other.state = nullptr; other.ime_param = nullptr; + other.extended_param = nullptr; + other.style_config = GetDefaultImeStyleConfig(); + other.menu_activate_armed = true; + other.l2_shortcut_armed = true; + other.prev_virtual_lstick_left_down = false; + other.prev_virtual_lstick_right_down = false; + other.prev_virtual_lstick_up_down = false; + other.prev_virtual_lstick_down_down = false; + other.left_stick_repeat_dir = 0; + other.left_stick_next_repeat_time = 0.0; + other.virtual_cross_next_repeat_time = 0.0; + other.virtual_triangle_next_repeat_time = 0.0; + other.prev_virtual_buttons = 0; + other.prev_virtual_square_down = false; + other.prev_virtual_l1_down = false; + other.prev_virtual_r1_down = false; + other.prev_virtual_dpad_left_down = false; + other.prev_virtual_dpad_right_down = false; + other.prev_virtual_dpad_up_down = false; + other.prev_virtual_dpad_down_down = false; + other.virtual_square_next_repeat_time = 0.0; + other.virtual_l1_next_repeat_time = 0.0; + other.virtual_r1_next_repeat_time = 0.0; + other.virtual_dpad_left_next_repeat_time = 0.0; + other.virtual_dpad_right_next_repeat_time = 0.0; + other.virtual_dpad_up_next_repeat_time = 0.0; + other.virtual_dpad_down_next_repeat_time = 0.0; + ResetImeEdgeWrapNav(other.panel_vertical_nav_state); + other.nav_layout_selection_initialized = false; + other.kb_alpha_family = ImeKbLayoutFamily::Latin; + other.gamepad_input_capture_active = false; AddLayer(this); + if (!gamepad_input_capture_active && ime_param) { + ImGui::Core::AcquireGamepadInputCapture(); + gamepad_input_capture_active = true; + } return *this; } @@ -206,87 +371,1319 @@ void ImeUi::Draw() { const auto& ctx = *GetCurrentContext(); const auto& io = ctx.IO; + const bool imgui_typing_mode_active = + native_input_active || request_input_focus || pending_input_selection_apply; + const bool ps4_typing_mode_active = !imgui_typing_mode_active; + const VirtualPadSnapshot virtual_pad = + ReadVirtualPadSnapshot(ime_param->user_id, io.DeltaTime, !ps4_typing_mode_active); - // TODO: Figure out how to properly translate the positions - - // for example, if a game wants to center the IME panel, - // we have to translate the panel position in a way that it - // still becomes centered, as the game normally calculates - // the position assuming a it's running on a 1920x1080 screen, - // whereas we are running on a 1280x720 window size (by default). - // - // e.g. Panel position calculation from a game: - // param.posx = (1920 / 2) - (panelWidth / 2); - // param.posy = (1080 / 2) - (panelHeight / 2); - const auto size = GetIO().DisplaySize; - f32 pos_x = (ime_param->posx / 1920.0f * (float)size.x); - f32 pos_y = (ime_param->posy / 1080.0f * (float)size.y); + const bool use_over2k = + (ime_param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT; + const auto viewport = Libraries::Ime::ComputeImeViewportMetrics(use_over2k); + const float scale_x = viewport.scale_x; + const float scale_y = viewport.scale_y; - ImVec2 window_pos = {pos_x, pos_y}; - ImVec2 window_size = {500.0f, 100.0f}; + u32 panel_req_w = 0; + u32 panel_req_h = 0; + (void)sceImeGetPanelSize(ime_param, &panel_req_w, &panel_req_h); - // SetNextWindowPos(window_pos); - SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), - ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + ImVec2 window_size{}; + if (panel_req_w > 0 && panel_req_h > 0) { + window_size = {panel_req_w * scale_x, panel_req_h * scale_y}; + } else { + window_size = {std::min(std::max(0.0f, viewport.size.x - 40.0f), 640.0f), + std::min(std::max(0.0f, viewport.size.y - 40.0f), 420.0f)}; + window_size.x = std::max(window_size.x, 320.0f); + window_size.y = std::max(window_size.y, 240.0f); + } + + float pos_x = viewport.offset.x + ime_param->posx * scale_x; + float pos_y = viewport.offset.y + ime_param->posy * scale_y; + if (ime_param->horizontal_alignment == OrbisImeHorizontalAlignment::Center) { + pos_x -= window_size.x * 0.5f; + } else if (ime_param->horizontal_alignment == OrbisImeHorizontalAlignment::Right) { + pos_x -= window_size.x; + } + if (ime_param->vertical_alignment == OrbisImeVerticalAlignment::Center) { + pos_y -= window_size.y * 0.5f; + } else if (ime_param->vertical_alignment == OrbisImeVerticalAlignment::Bottom) { + pos_y -= window_size.y; + } + const float min_x = viewport.offset.x; + const float max_x = viewport.offset.x + std::max(0.0f, viewport.size.x - window_size.x); + const float min_y = viewport.offset.y; + const float max_y = viewport.offset.y + std::max(0.0f, viewport.size.y - window_size.y); + pos_x = std::clamp(pos_x, min_x, max_x); + pos_y = std::clamp(pos_y, min_y, max_y); + + const bool panel_position_locked = True(ime_param->option & OrbisImeOption::FIXED_POSITION); + if (!panel_position_initialized) { + panel_position = {pos_x, pos_y}; + panel_position_initialized = true; + } + if (panel_position_locked) { + panel_position = {pos_x, pos_y}; + panel_drag_active = false; + } else { + if (!ps4_typing_mode_active) { + panel_drag_active = false; + } else { + const ImVec2 mouse_pos = io.MousePos; + const bool mouse_over_panel = mouse_pos.x >= panel_position.x && + mouse_pos.x <= (panel_position.x + window_size.x) && + mouse_pos.y >= panel_position.y && + mouse_pos.y <= (panel_position.y + window_size.y); + if (!panel_drag_active && IsMouseClicked(ImGuiMouseButton_Left, false) && + mouse_over_panel) { + panel_drag_active = true; + } + if (panel_drag_active) { + if (IsMouseDown(ImGuiMouseButton_Left)) { + panel_position.x += io.MouseDelta.x; + panel_position.y += io.MouseDelta.y; + } else { + panel_drag_active = false; + } + } + const ImVec2 right_stick_delta = virtual_pad.panel_delta; + panel_position.x += right_stick_delta.x; + panel_position.y += right_stick_delta.y; + } + } + panel_position.x = std::clamp(panel_position.x, min_x, max_x); + panel_position.y = std::clamp(panel_position.y, min_y, max_y); + + SetNextWindowPos(panel_position); SetNextWindowSize(window_size); + SetNextWindowDockID(0, ImGuiCond_Always); SetNextWindowCollapsed(false); if (first_render || !io.NavActive) { SetNextWindowFocus(); } - if (Begin("IME##Ime", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoSavedSettings)) { + const auto lock_window_scroll = []() { + SetScrollX(0.0f); + SetScrollY(0.0f); + ImGuiWindow* window = GetCurrentWindow(); + if (!window) { + return; + } + window->Scroll = ImVec2(0.0f, 0.0f); + const float max_f = std::numeric_limits::max(); + window->ScrollTarget = ImVec2(max_f, max_f); + }; + + ImGuiWindowFlags window_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + window_flags |= ImGuiWindowFlags_NoNavInputs; + if (Begin("IME##Ime", nullptr, window_flags)) { + lock_window_scroll(); + KeepNavHighlight(); DrawPrettyBackground(); + const Libraries::Ime::ImePanelMetricsConfig metrics_cfg{ + .panel_w = window_size.x, + .panel_h = window_size.y, + .multiline = True(ime_param->option & OrbisImeOption::MULTILINE), + .show_title = false, + .base_font_size = GetFontSize(), + .window_pos = GetWindowPos(), + }; + const Libraries::Ime::ImePanelMetrics metrics = + Libraries::Ime::ComputeImePanelMetrics(metrics_cfg); - DrawInputText(); - SetCursorPosY(GetCursorPosY() + 10.0f); + const bool controller_shortcuts_disabled = + extended_param && + True(extended_param->disable_device & OrbisImeDisableDevice::CONTROLLER); + const bool allow_osk_shortcuts = ps4_typing_mode_active && !controller_shortcuts_disabled; + OskPadInputState panel_pad_state{ + prev_virtual_buttons, + prev_virtual_cross_down, + prev_virtual_lstick_left_down, + prev_virtual_lstick_right_down, + prev_virtual_lstick_up_down, + prev_virtual_lstick_down_down, + left_stick_repeat_dir, + left_stick_next_repeat_time, + virtual_cross_next_repeat_time, + prev_virtual_dpad_left_down, + prev_virtual_dpad_right_down, + prev_virtual_dpad_up_down, + prev_virtual_dpad_down_down, + virtual_dpad_left_next_repeat_time, + virtual_dpad_right_next_repeat_time, + virtual_dpad_up_next_repeat_time, + virtual_dpad_down_next_repeat_time, + }; + const OskPadInputFrame panel_input = ComputeOskPadInputFrame( + virtual_pad, allow_osk_shortcuts, first_render, panel_pad_state); - const char* button_text; - button_text = "Done##ImeDone"; + const OskVirtualPadInputView virtual_pad_input(panel_input, io); + const bool cross_down = panel_input.cross_down; + const bool panel_activate_pressed_raw = panel_input.panel_activate_pressed_raw; + const bool panel_activate_repeat_raw = panel_input.panel_activate_repeat_raw; + const bool nav_left = panel_input.nav_left; + const bool nav_right = panel_input.nav_right; + const bool nav_up = panel_input.nav_up; + const bool nav_down = panel_input.nav_down; + const bool nav_left_repeat = panel_input.nav_left_repeat; + const bool nav_right_repeat = panel_input.nav_right_repeat; + const bool nav_up_repeat = panel_input.nav_up_repeat; + const bool nav_down_repeat = panel_input.nav_down_repeat; + const bool virtual_control_input = panel_input.virtual_control_input; + const bool osk_control_input = panel_input.osk_control_input; - 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; + const bool raw_osk_control_input = panel_input.raw_osk_control_input; + if (native_input_active && raw_osk_control_input && !request_input_focus && + !pending_input_selection_apply) { + native_input_active = false; + request_input_focus = false; + pending_input_selection_apply = false; + ImGui::ClearActiveID(); + } + const double nav_now = ImGui::GetTime(); + const bool cancel_shortcut_pressed = + allow_osk_shortcuts && + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::Circle); + const ImVec2 mouse_delta = ImGui::GetIO().MouseDelta; + const bool pointer_input = IsMouseClicked(ImGuiMouseButton_Left, false) || + IsMouseClicked(ImGuiMouseButton_Right, false) || + IsMouseClicked(ImGuiMouseButton_Middle, false) || + (mouse_delta.x != 0.0f || mouse_delta.y != 0.0f); + if (pointer_input) { + pointer_navigation_active = true; + } + if (osk_control_input) { + pointer_navigation_active = false; + } + if (native_input_active && virtual_control_input && !request_input_focus && + !pending_input_selection_apply) { + native_input_active = false; + ImGui::ClearActiveID(); + } + if (!accept_armed) { + if (!cross_down) { + accept_armed = true; + } + } + const bool panel_activate_pressed = + allow_osk_shortcuts && accept_armed && + (panel_activate_pressed_raw || panel_activate_repeat_raw); - SetCursorPosX(button_start_pos); + using SelectionIndex = ImeSelectionGridIndex; + const auto& selected_kb_layout = Libraries::Ime::GetImeKeyboardLayout(kb_layout_selection); + const bool nav_layout_changed = + nav_layout_selection_initialized && + (last_nav_layout_selection.family != kb_layout_selection.family || + last_nav_layout_selection.case_state != kb_layout_selection.case_state || + last_nav_layout_selection.page != kb_layout_selection.page); + if (nav_layout_changed) { + ResetImeEdgeWrapNav(panel_vertical_nav_state); + } + last_nav_layout_selection = kb_layout_selection; + nav_layout_selection_initialized = true; + const int keyboard_min_col = 0; + const int keyboard_min_row = 0; + const int keyboard_cols = std::max(1, static_cast(selected_kb_layout.cols)); + const int keyboard_rows = std::max(1, static_cast(selected_kb_layout.rows)); + const int keyboard_max_col = keyboard_cols - 1; + const int keyboard_max_row = keyboard_rows - 1; + const auto keyboard_vertical_wraps_from = [&](int from_row, int from_col, int step_row) { + return DoesImeKeyboardNavigationWrap(selected_kb_layout, from_row, from_col, step_row, + 0); + }; - if (Button(button_text, BUTTON_SIZE) || (IsKeyPressed(ImGuiKey_Enter))) { - state->SendEnterEvent(); + const auto& top_layout_cfg = + Libraries::Ime::GetImeTopPanelLayoutConfig(kb_layout_selection); + const int top_panel_row = + SelectionIndex::PanelTopRowFromConfig(static_cast(top_layout_cfg.row)); + const int top_panel_rows = + SelectionIndex::PanelTopRowsFromConfig(static_cast(top_layout_cfg.row_span)); + const int keyboard_panel_min_row = + SelectionIndex::PanelKeyboardMinRowForTopPanel(top_panel_row, top_panel_rows); + const int keyboard_panel_max_row = SelectionIndex::PanelKeyboardMaxRowForKeyboardRows( + keyboard_rows, top_panel_row, top_panel_rows); + const int top_col_min = 0; + const int top_cols_cfg = std::max(1, static_cast(top_layout_cfg.cols)); + const int top_col_max = top_col_min + top_cols_cfg - 1; + + const auto top_to_keyboard_col = [&](int top_col) { + const int clamped_top = std::clamp(top_col, top_col_min, top_col_max); + if (keyboard_cols <= 1 || top_cols_cfg <= 1) { + return keyboard_min_col; + } + const int top_offset = clamped_top - top_col_min; + return keyboard_min_col + (top_offset * (keyboard_cols - 1)) / (top_cols_cfg - 1); + }; + const auto keyboard_to_top_col = [&](int keyboard_col) { + const int clamped_kb = std::clamp(keyboard_col, keyboard_min_col, keyboard_max_col); + if (top_cols_cfg <= 1 || keyboard_cols <= 1) { + return top_col_min; + } + const int kb_offset = clamped_kb - keyboard_min_col; + return top_col_min + (kb_offset * (top_cols_cfg - 1)) / (keyboard_cols - 1); + }; + + struct TopNavElement { + PanelSelectionTarget target = PanelSelectionTarget::Prediction; + int min_col = 0; + int max_col = 0; + }; + + std::vector top_col_to_element_index(static_cast(top_cols_cfg), -1); + std::vector top_elements; + top_elements.reserve(static_cast(top_cols_cfg)); + + const auto append_top_element = [&](ImeTopPanelElementId id, int min_col, int max_col) { + PanelSelectionTarget target = PanelSelectionTarget::Prediction; + switch (id) { + case ImeTopPanelElementId::Prediction: + target = PanelSelectionTarget::Prediction; + break; + case ImeTopPanelElementId::Close: + target = PanelSelectionTarget::Close; + break; + default: + return; + } + + if (static_cast(top_elements.size()) >= top_cols_cfg) { + return; + } + if (max_col < min_col) { + return; + } + + const int clamped_min = std::clamp(min_col, top_col_min, top_col_max); + const int clamped_max = std::clamp(max_col, top_col_min, top_col_max); + if (clamped_max < clamped_min) { + return; + } + + top_elements.push_back({target, clamped_min, clamped_max}); + const int element_index = static_cast(top_elements.size()) - 1; + for (int col = clamped_min; col <= clamped_max; ++col) { + top_col_to_element_index[static_cast(col - top_col_min)] = + element_index; + } + }; + + for (std::size_t i = 0; i < top_layout_cfg.element_count; ++i) { + const auto& spec = top_layout_cfg.elements[i]; + const int min_col = std::clamp(static_cast(spec.col), top_col_min, top_col_max); + const int span = std::max(1, static_cast(spec.col_span)); + const int max_col = std::clamp(min_col + span - 1, top_col_min, top_col_max); + append_top_element(spec.id, min_col, max_col); + } + if (top_elements.empty()) { + append_top_element(ImeTopPanelElementId::Prediction, top_col_min, top_col_max); } - SameLine(0.0f, button_spacing); + const auto element_index_for_col = [&](int col) { + if (col < top_col_min || col > top_col_max) { + return -1; + } + return top_col_to_element_index[static_cast(col - top_col_min)]; + }; + const auto element_index_for_target = [&](PanelSelectionTarget target) { + for (int i = 0; i < static_cast(top_elements.size()); ++i) { + if (top_elements[static_cast(i)].target == target) { + return i; + } + } + return -1; + }; + const auto set_top_selection = [&](PanelSelectionTarget target, int preferred_col) { + panel_selection = target; + const int target_idx = element_index_for_target(target); + if (target_idx < 0) { + top_virtual_col = std::clamp(preferred_col, top_col_min, top_col_max); + return; + } + const auto& element = top_elements[static_cast(target_idx)]; + top_virtual_col = std::clamp(preferred_col, element.min_col, element.max_col); + }; + const auto top_col_for_selection = [&](PanelSelectionTarget target) { + const int target_idx = element_index_for_target(target); + const int clamped_col = std::clamp(top_virtual_col, top_col_min, top_col_max); + if (target_idx < 0) { + return clamped_col; + } + const auto& element = top_elements[static_cast(target_idx)]; + return std::clamp(clamped_col, element.min_col, element.max_col); + }; + const bool menu_modal = (edit_menu_popup != EditMenuPopup::None); + const int keyboard_row_before_panel_nav_raw = + (pending_keyboard_row >= keyboard_min_row && pending_keyboard_row <= keyboard_max_row) + ? pending_keyboard_row + : last_keyboard_selected_row; + const int keyboard_col_before_panel_nav_raw = + (pending_keyboard_col >= keyboard_min_col && pending_keyboard_col <= keyboard_max_col) + ? pending_keyboard_col + : last_keyboard_selected_col; + const int keyboard_row_before_panel_nav = + std::clamp(keyboard_row_before_panel_nav_raw, keyboard_min_row, keyboard_max_row); + const int keyboard_col_before_panel_nav = + std::clamp(keyboard_col_before_panel_nav_raw, keyboard_min_col, keyboard_max_col); + bool entered_top_from_keyboard = false; + const auto move_keyboard_edge_to_top = [&](int wrap_dir_y, int keyboard_col) { + const int top_col = keyboard_to_top_col(keyboard_col); + const int element_idx = element_index_for_col(top_col); + if (element_idx >= 0) { + const auto& element = top_elements[static_cast(element_idx)]; + set_top_selection(element.target, top_col); + } else if (!top_elements.empty()) { + const auto& first_element = top_elements[0]; + set_top_selection(first_element.target, first_element.min_col); + } + CommitImeEdgeWrapStep(panel_vertical_nav_state, wrap_dir_y, 0, nav_now); + entered_top_from_keyboard = true; + }; + if (!menu_modal && !text_select_mode && !pointer_navigation_active && + panel_selection == PanelSelectionTarget::Keyboard) { + const int wrap_dir_y = + (nav_up && keyboard_vertical_wraps_from(keyboard_row_before_panel_nav, + keyboard_col_before_panel_nav, -1)) + ? -1 + : ((nav_down && keyboard_vertical_wraps_from(keyboard_row_before_panel_nav, + keyboard_col_before_panel_nav, 1)) + ? 1 + : 0); + if (wrap_dir_y != 0) { + const bool hold_before_wrap_to_top = wrap_dir_y > 0; + const bool wrap_repeat = nav_down_repeat; + if (!hold_before_wrap_to_top || + !ShouldDelayImeEdgeWrap(panel_vertical_nav_state, wrap_dir_y, 0, wrap_repeat, + true, nav_now, kPanelEdgeWrapHoldDelaySec, + kRepeatIntentWindowSec)) { + move_keyboard_edge_to_top(wrap_dir_y, keyboard_col_before_panel_nav); + } + } + } - if (Button("Close##ImeClose", BUTTON_SIZE)) { + SetWindowFontScale(std::max(viewport.ui_scale, metrics.input_font_scale)); + const bool input_hovered = DrawInputText(metrics, pointer_navigation_active); + const bool input_selected = + pointer_navigation_active && (input_hovered || native_input_active); + const bool input_clicked = pointer_navigation_active && input_hovered && + IsMouseClicked(ImGuiMouseButton_Left, false); + static std::unordered_map s_selector_fade_states; + const auto draw_selector = [&](const char* selector_id, ImVec2 pos, ImVec2 size, + bool selected, bool pulse_triggered) { + auto& fade_state = s_selector_fade_states[ImGui::GetID(selector_id)]; + const double now = ImGui::GetTime(); + if (selected && pulse_triggered) { + TriggerSelectorPressPulse(fade_state, now); + } + const float selector_corner_radius = + std::max(0.0f, metrics.corner_radius - kSelectorInnerMargin); + UpdateSelectorFadeState(fade_state, pos, size, kSelectorInnerMargin, + selector_corner_radius, selected, now); + const float press_pulse_expand = + selected + ? ComputePressPulseExpand( + fade_state.press_pulse_started_at, now, kSelectorPressPulseDurationSec, + kSelectorBorderThickness * kSelectorPressPulseExpandBorderFactor) + : 0.0f; + DrawSelectorFadeState(fade_state, GetWindowDrawList(), kSelectorOverlayColor, + kSelectorBorderColor, kSelectorBorderThickness, + kSelectorFadeOutDurationSec, now, press_pulse_expand); + }; + draw_selector(kSelectorInputId, metrics.input_pos_screen, metrics.input_size, + input_selected, + input_clicked || (panel_selection == PanelSelectionTarget::Input && + panel_activate_pressed_raw)); + + auto* draw = GetWindowDrawList(); + const ImU32 pane_bg = ImeColorToImU32(style_config.color_base); + 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); + SetCursorScreenPos(metrics.predict_pos); + PushID("##ImePredict"); + PushItemFlag(ImGuiItemFlags_NoNav, true); + InvisibleButton("##ImePredict", metrics.predict_size); + PopItemFlag(); + const bool prediction_clicked = + IsMouseClicked(ImGuiMouseButton_Left, false) && IsItemHovered(); + const int prediction_element_idx = + element_index_for_target(PanelSelectionTarget::Prediction); + if (pointer_navigation_active && prediction_clicked && prediction_element_idx >= 0) { + const auto& prediction_element = + top_elements[static_cast(prediction_element_idx)]; + set_top_selection(PanelSelectionTarget::Prediction, + SelectionIndex::GridColumnFromX( + io.MousePos.x, metrics.predict_pos.x, metrics.predict_size.x, + prediction_element.min_col, prediction_element.max_col)); + } + PopID(); + draw_selector(kSelectorPredictId, metrics.predict_pos, metrics.predict_size, + panel_selection == PanelSelectionTarget::Prediction, + prediction_clicked || (panel_selection == PanelSelectionTarget::Prediction && + panel_activate_pressed_raw)); + const ImU32 close_button_bg = ImeColorToImU32(style_config.color_button_function); + PushStyleColor(ImGuiCol_Button, BrightenColor(close_button_bg, 0.0f)); + PushStyleColor(ImGuiCol_ButtonHovered, BrightenColor(close_button_bg, 0.08f)); + PushStyleColor(ImGuiCol_ButtonActive, BrightenColor(close_button_bg, 0.16f)); + SetCursorScreenPos(metrics.close_pos); + PushItemFlag(ImGuiItemFlags_NoNav, true); + bool cancel_pressed = Button("##ImeClose", {metrics.close_size.x, metrics.close_size.y}); + PopItemFlag(); + constexpr const char* kCloseLabel = "\xE2\x9C\x95"; + const ImVec2 close_label_size = CalcTextSize(kCloseLabel, nullptr, true); + const float close_pad_y = std::max(1.0f, metrics.close_size.y * 0.04f); + const ImVec2 close_label_pos{metrics.close_pos.x + + (metrics.close_size.x - close_label_size.x) * 0.5f, + metrics.close_pos.y + close_pad_y}; + draw->AddText(close_label_pos, ImeColorToImU32(style_config.color_text), kCloseLabel); + cancel_pressed = cancel_pressed || cancel_shortcut_pressed; + const bool close_clicked = IsMouseClicked(ImGuiMouseButton_Left, false) && IsItemHovered(); + const int close_element_idx = element_index_for_target(PanelSelectionTarget::Close); + if (pointer_navigation_active && close_clicked && close_element_idx >= 0) { + const auto& close_element = top_elements[static_cast(close_element_idx)]; + set_top_selection(PanelSelectionTarget::Close, close_element.min_col); + } + PopStyleColor(3); + draw_selector(kSelectorCloseId, metrics.close_pos, metrics.close_size, + panel_selection == PanelSelectionTarget::Close, + close_clicked || (panel_selection == PanelSelectionTarget::Close && + panel_activate_pressed_raw)); + if (!cancel_pressed && panel_selection == PanelSelectionTarget::Close && + panel_activate_pressed) { + cancel_pressed = true; + } + + SetCursorScreenPos(metrics.kb_pos); + + bool entered_keyboard_from_top = false; + const auto move_top_navigation = [&](int dir_x, int dir_y) { + const PanelSelectionTarget current = panel_selection; + int col = top_col_for_selection(current); + int origin_element_idx = element_index_for_col(col); + if (origin_element_idx < 0) { + origin_element_idx = element_index_for_target(current); + } + + if (dir_x != 0) { + if (origin_element_idx >= 0) { + const int span = top_col_max - top_col_min + 1; + bool crossed_wrap = false; + for (int step = 1; step <= span; ++step) { + const int next_col_raw = col + dir_x * step; + crossed_wrap = crossed_wrap || next_col_raw < top_col_min || + next_col_raw > top_col_max; + const int next_col = + top_col_min + (col - top_col_min + dir_x * step + span) % span; + const int next_element_idx = element_index_for_col(next_col); + if (next_element_idx < 0 || next_element_idx == origin_element_idx) { + continue; + } + const bool repeat_hint = dir_x < 0 ? nav_left_repeat : nav_right_repeat; + if (ShouldDelayImeEdgeWrap( + panel_vertical_nav_state, 0, dir_x, repeat_hint, crossed_wrap, + nav_now, kPanelEdgeWrapHoldDelaySec, kRepeatIntentWindowSec)) { + return; + } + const auto& next_element = + top_elements[static_cast(next_element_idx)]; + set_top_selection(next_element.target, next_col); + CommitImeEdgeWrapStep(panel_vertical_nav_state, 0, dir_x, nav_now); + return; + } + } + } + + if (origin_element_idx >= 0) { + if (dir_y > 0) { + pending_keyboard_row = SelectionIndex::PanelToKeyboardRow( + keyboard_panel_min_row, keyboard_rows, top_panel_row, top_panel_rows); + pending_keyboard_col = top_to_keyboard_col(col); + panel_selection = PanelSelectionTarget::Keyboard; + entered_keyboard_from_top = true; + CommitImeEdgeWrapStep(panel_vertical_nav_state, dir_y, 0, nav_now); + return; + } + if (dir_y < 0) { + if (ShouldDelayImeEdgeWrap(panel_vertical_nav_state, dir_y, 0, nav_up_repeat, + true, nav_now, kPanelEdgeWrapHoldDelaySec, + kRepeatIntentWindowSec)) { + return; + } + pending_keyboard_row = SelectionIndex::PanelToKeyboardRow( + keyboard_panel_max_row, keyboard_rows, top_panel_row, top_panel_rows); + pending_keyboard_col = top_to_keyboard_col(col); + panel_selection = PanelSelectionTarget::Keyboard; + entered_keyboard_from_top = true; + CommitImeEdgeWrapStep(panel_vertical_nav_state, dir_y, 0, nav_now); + return; + } + } + }; + if (!menu_modal && !text_select_mode && !pointer_navigation_active && + !entered_top_from_keyboard && panel_selection != PanelSelectionTarget::Keyboard) { + if (nav_left) { + move_top_navigation(-1, 0); + } else if (nav_right) { + move_top_navigation(1, 0); + } else if (nav_up) { + move_top_navigation(0, -1); + } else if (nav_down) { + move_top_navigation(0, 1); + } + } + + bool accept_pressed = false; + auto sync_text_buffers = [&]() { + if (!state->current_text.begin()) { + return; + } + state->ConvertUTF8ToOrbis(state->current_text.begin(), state->current_text.size(), + reinterpret_cast(state->work_buffer), + state->max_text_length + 1); + if (state->text_buffer) { + state->ConvertUTF8ToOrbis(state->current_text.begin(), state->current_text.size(), + state->text_buffer, state->max_text_length + 1); + } + }; + const auto text_length_utf16 = [&]() { + const char* text = state->current_text.begin(); + const int byte_len = static_cast(state->current_text.size()); + return Utf16CountFromUtf8Range(text, text ? (text + byte_len) : nullptr); + }; + const auto emit_update_text_event = [&](int edit_index_utf16, int edit_delta_utf16, + int caret_utf16) { + OrbisImeEditText event_param{}; + event_param.str = reinterpret_cast(state->work_buffer); + event_param.area_num = 1; + event_param.caret_index = static_cast(std::max(0, caret_utf16)); + event_param.text_area[0].mode = OrbisImeTextAreaMode::Edit; + event_param.text_area[0].index = static_cast(std::max(0, edit_index_utf16)); + event_param.text_area[0].length = static_cast(edit_delta_utf16); + + OrbisImeEvent event{}; + event.id = OrbisImeEventId::UpdateText; + event.param.text = event_param; + state->SendEvent(&event); + }; + const auto emit_update_caret_events = [&](int old_caret_utf16, int new_caret_utf16) { + const int delta = new_caret_utf16 - old_caret_utf16; + if (delta == 0) { + return; + } + const bool move_right = delta > 0; + const u32 steps = static_cast(std::abs(delta)); + const OrbisImeCaretMovementDirection direction = + move_right ? OrbisImeCaretMovementDirection::Right + : OrbisImeCaretMovementDirection::Left; + for (u32 i = 0; i < steps; ++i) { + OrbisImeEvent caret_step{}; + caret_step.id = OrbisImeEventId::UpdateCaret; + caret_step.param.caret_move = direction; + state->SendEvent(&caret_step); + } + }; + const auto apply_selection_state = [&]() { + const int len = text_length_utf16(); + const int caret = std::clamp( + (text_select_focus_utf16 >= 0) + ? text_select_focus_utf16 + : ((state->caret_index >= 0) ? state->caret_index : input_cursor_utf16), + 0, len); + const int anchor = + text_select_mode ? std::clamp(text_select_anchor_utf16, 0, len) : caret; + const int focus = + text_select_mode ? std::clamp(text_select_focus_utf16, 0, len) : caret; + const char* text = state->current_text.begin(); + const int anchor_byte = Utf8ByteIndexFromUtf16Index(text ? text : "", anchor); + const int focus_byte = Utf8ByteIndexFromUtf16Index(text ? text : "", focus); + input_cursor_utf16 = focus; + input_cursor_byte = focus_byte; + input_selection_start_byte = std::min(anchor_byte, focus_byte); + input_selection_end_byte = std::max(anchor_byte, focus_byte); + state->caret_index = focus; + state->caret_byte_index = focus_byte; + state->caret_dirty = native_input_active; + if (native_input_active) { + pending_input_selection_apply = true; + request_input_focus = true; + } else { + pending_input_selection_apply = false; + request_input_focus = false; + } + }; + const auto apply_text_edit = [&](int start_utf16, int end_utf16, + const char* insert_utf8) -> bool { + const std::string current = state->current_text.to_string(); + const char* current_text = current.c_str(); + const int len_utf16 = Utf16CountFromUtf8Range( + current_text, current_text + static_cast(current.size())); + const int start = std::clamp(start_utf16, 0, len_utf16); + const int end = std::clamp(end_utf16, start, len_utf16); + const int removed_utf16 = end - start; + const int available_utf16 = + static_cast(state->max_text_length) - (len_utf16 - removed_utf16); + + std::string insert_clamped{}; + if (insert_utf8 && insert_utf8[0] != '\0' && available_utf16 > 0) { + const char* p = insert_utf8; + int used_utf16 = 0; + while (*p && used_utf16 < available_utf16) { + unsigned int codepoint = 0; + const int step = ImTextCharFromUtf8(&codepoint, p, nullptr); + if (step <= 0) { + break; + } + const int units = (codepoint > 0xFFFF) ? 2 : 1; + if (used_utf16 + units > available_utf16) { + break; + } + insert_clamped.append(p, static_cast(step)); + p += step; + used_utf16 += units; + } + } + + if (removed_utf16 == 0 && insert_clamped.empty()) { + return false; + } + + const int start_byte = Utf8ByteIndexFromUtf16Index(current_text, start); + const int end_byte = Utf8ByteIndexFromUtf16Index(current_text, end); + std::string updated{}; + updated.reserve(current.size() - static_cast(end_byte - start_byte) + + insert_clamped.size()); + updated.append(current, 0, static_cast(start_byte)); + updated.append(insert_clamped); + updated.append(current, static_cast(end_byte), std::string::npos); + + state->current_text.FromString(updated); + sync_text_buffers(); + const int inserted_utf16 = Utf16CountFromUtf8Range( + insert_clamped.c_str(), + insert_clamped.c_str() + static_cast(insert_clamped.size())); + const int new_caret_utf16 = start + inserted_utf16; + text_select_mode = false; + text_select_anchor_utf16 = new_caret_utf16; + text_select_focus_utf16 = new_caret_utf16; + apply_selection_state(); + emit_update_text_event(start, inserted_utf16 - removed_utf16, input_cursor_utf16); + return true; + }; + const auto selection_utf16_range = [&]() -> std::pair { + const int len = text_length_utf16(); + if (!text_select_mode) { + const int caret = std::clamp(state->caret_index, 0, len); + return {caret, caret}; + } + const int anchor = std::clamp(text_select_anchor_utf16, 0, len); + const int focus = std::clamp(text_select_focus_utf16, 0, len); + return {std::min(anchor, focus), std::max(anchor, focus)}; + }; + const auto insert_text_at_caret = [&](const char* suffix) { + const auto [sel_start, sel_end] = selection_utf16_range(); + return apply_text_edit(sel_start, sel_end, suffix); + }; + const auto backspace_at_caret = [&]() { + const auto [sel_start, sel_end] = selection_utf16_range(); + if (sel_end > sel_start) { + return apply_text_edit(sel_start, sel_end, ""); + } + if (sel_end <= 0) { + return false; + } + const std::string current = state->current_text.to_string(); + const char* text = current.c_str(); + const int caret_byte = Utf8ByteIndexFromUtf16Index(text, sel_end); + int prev_byte = caret_byte; + do { + --prev_byte; + } while (prev_byte > 0 && + (static_cast(text[prev_byte]) & 0xC0u) == 0x80u); + const int prev_utf16 = Utf16CountFromUtf8Range(text, text + prev_byte); + return apply_text_edit(prev_utf16, sel_end, ""); + }; + const auto clear_all_text = [&]() { + const int len = text_length_utf16(); + if (len <= 0) { + return false; + } + // Matches libSceIme backend all-delete behavior (sceImeBackendAllDeleteConvertString): + // clear the whole editable string in one operation. + return apply_text_edit(0, len, ""); + }; + + 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.cols = keyboard_cols; + kb_layout.rows = keyboard_rows; + const int layout_rows = std::max(1, kb_layout.rows); + kb_layout.fixed_bottom_rows = SelectionIndex::ResolveFunctionRows( + layout_rows, static_cast(selected_kb_layout.function_rows)); + if (kb_layout.fixed_bottom_rows > 0) { + kb_layout.bottom_row_h = std::max(8.0f, metrics.key_h); + const int typing_rows = std::max(1, layout_rows - kb_layout.fixed_bottom_rows); + const float typing_area_h = + kb_layout.size.y - kb_layout.key_gap_y * static_cast(layout_rows - 1) - + kb_layout.bottom_row_h * static_cast(kb_layout.fixed_bottom_rows); + const float computed_typing_key_h = typing_area_h / static_cast(typing_rows); + kb_layout.key_h = std::max(8.0f, computed_typing_key_h); + } else { + kb_layout.fixed_bottom_rows = 0; + kb_layout.bottom_row_h = 0.0f; + const float computed_key_h = + (kb_layout.size.y - kb_layout.key_gap_y * static_cast(layout_rows - 1)) / + static_cast(layout_rows); + kb_layout.key_h = std::max(8.0f, computed_key_h); + } + kb_layout.corner_radius = metrics.corner_radius; + + Libraries::Ime::ImeKbDrawParams kb_params{}; + kb_params.selection = kb_layout_selection; + kb_params.layout_model = &selected_kb_layout; + kb_params.supported_languages = ime_param->supported_languages; + kb_params.enter_label = ime_param->enter_label; + kb_params.show_selection_highlight = (panel_selection == PanelSelectionTarget::Keyboard); + kb_params.allow_nav_input = allow_osk_shortcuts && !menu_modal && !text_select_mode && + (panel_selection == PanelSelectionTarget::Keyboard) && + !entered_keyboard_from_top; + kb_params.use_imgui_lstick_nav = false; + kb_params.allow_activate_input = allow_osk_shortcuts && accept_armed && !menu_modal && + !text_select_mode && + (panel_selection == PanelSelectionTarget::Keyboard); + ApplyOskPanelNavToKeyboardParams(kb_params, allow_osk_shortcuts, panel_input); + kb_params.external_activate_pressed = panel_activate_pressed; + kb_params.external_activate_repeat = + allow_osk_shortcuts && accept_armed && panel_activate_repeat_raw; + kb_params.reset_nav_state = nav_layout_changed; + kb_params.requested_selected_row = pending_keyboard_row; + kb_params.requested_selected_col = pending_keyboard_col; + ApplyImeStyleToKeyboardDrawParams(style_config, kb_params); + pending_keyboard_row = -1; + pending_keyboard_col = -1; + + 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.selected_row >= 0 && kb_state.selected_col >= 0) { + last_keyboard_selected_row = kb_state.selected_row; + last_keyboard_selected_col = kb_state.selected_col; + } + if (pointer_navigation_active && kb_state.clicked) { + panel_selection = PanelSelectionTarget::Keyboard; + } + + const auto consume_temporary_uppercase = [&](bool typed_character) { + if (typed_character && kb_layout_selection.case_state == ImeKbCaseState::Upper) { + kb_layout_selection.case_state = ImeKbCaseState::Lower; + } + }; + const auto has_clipboard_text = [&]() { + const char* text = ImGui::GetClipboardText(); + return text && text[0] != '\0'; + }; + const auto get_selection_byte_range = [&]() { + const int text_len = static_cast(state->current_text.size()); + const int sel_start = std::clamp(input_selection_start_byte, 0, text_len); + const int sel_end = std::clamp(input_selection_end_byte, 0, text_len); + return std::pair{std::min(sel_start, sel_end), std::max(sel_start, sel_end)}; + }; + const auto copy_selected_text = [&]() { + const std::string current = state->current_text.to_string(); + const auto [sel_start, sel_end] = get_selection_byte_range(); + if (sel_end > sel_start) { + const std::string selection = + current.substr(static_cast(sel_start), + static_cast(sel_end - sel_start)); + ImGui::SetClipboardText(selection.c_str()); + } else { + ImGui::SetClipboardText(current.c_str()); + } + }; + const auto collapse_selection_to_caret = [&](int caret_utf16) { + const int len = text_length_utf16(); + const int clamped_caret = std::clamp(caret_utf16, 0, len); + text_select_mode = false; + text_select_anchor_utf16 = clamped_caret; + text_select_focus_utf16 = clamped_caret; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto move_text_caret = [&](int delta_utf16, bool preserve_selection) { + const int len = text_length_utf16(); + int base = text_select_mode ? text_select_focus_utf16 : state->caret_index; + if (base < 0) { + base = state->caret_index; + } + base = std::clamp(base, 0, len); + const int next = std::clamp(base + delta_utf16, 0, len); + if (preserve_selection && text_select_mode) { + text_select_focus_utf16 = next; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + emit_update_caret_events(base, next); + return next != base; + } + collapse_selection_to_caret(next); + emit_update_caret_events(base, next); + return next != base; + }; + const auto move_text_caret_to_boundary = [&](bool to_end, bool preserve_selection) { + const int len = text_length_utf16(); + int base = text_select_mode ? text_select_focus_utf16 : state->caret_index; + if (base < 0) { + base = state->caret_index; + } + base = std::clamp(base, 0, len); + const int next = to_end ? len : 0; + if (preserve_selection && text_select_mode) { + text_select_focus_utf16 = next; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + emit_update_caret_events(base, next); + return next != base; + } + collapse_selection_to_caret(next); + emit_update_caret_events(base, next); + return next != base; + }; + const auto begin_text_selection_from_caret = [&]() { + text_select_mode = true; + const int len = text_length_utf16(); + const int caret = std::clamp(state->caret_index, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto select_all_text = [&]() { + text_select_mode = true; + const int len = text_length_utf16(); + text_select_anchor_utf16 = 0; + text_select_focus_utf16 = len; + apply_selection_state(); + panel_selection = PanelSelectionTarget::Keyboard; + }; + const auto open_main_menu = [&]() { + OpenOskMainEditMenu(edit_menu_popup, edit_menu_index, menu_activate_armed); + }; + const auto open_actions_menu = [&]() { + OpenOskActionsEditMenu(edit_menu_popup, edit_menu_index, menu_activate_armed); + }; + const auto apply_main_menu_action = [&](int action_index) { + switch (action_index) { + case 0: // Select + begin_text_selection_from_caret(); + edit_menu_popup = EditMenuPopup::None; + break; + case 1: // Select All + select_all_text(); + open_actions_menu(); + break; + case 2: // Paste + if (has_clipboard_text()) { + (void)insert_text_at_caret(ImGui::GetClipboardText()); + } + edit_menu_popup = EditMenuPopup::None; + break; + default: + break; + } + }; + const auto apply_actions_menu_action = [&](int action_index) { + switch (action_index) { + case 0: // Copy + copy_selected_text(); + collapse_selection_to_caret(text_select_focus_utf16 >= 0 ? text_select_focus_utf16 + : input_cursor_utf16); + edit_menu_popup = EditMenuPopup::None; + break; + case 1: // Paste + if (has_clipboard_text()) { + (void)insert_text_at_caret(ImGui::GetClipboardText()); + } + edit_menu_popup = EditMenuPopup::None; + break; + default: + break; + } + }; + + bool opened_menu_this_frame = false; + OskShortcutRepeatState shortcut_repeat_state{prev_virtual_square_down, + prev_virtual_l1_down, + prev_virtual_r1_down, + l2_shortcut_armed, + virtual_square_next_repeat_time, + virtual_l1_next_repeat_time, + virtual_r1_next_repeat_time, + virtual_triangle_next_repeat_time}; + const OskShortcutActionResult shortcut_action = EvaluateOskShortcutAction( + allow_osk_shortcuts, menu_modal, + kb_state.pressed_action == Libraries::Ime::ImeKbKeyAction::None, panel_input, + virtual_pad_input, prev_virtual_buttons, kb_layout_selection.family, + shortcut_repeat_state); + bool keyboard_action_from_hotkey = false; + if (shortcut_action.clear_all) { + (void)clear_all_text(); + } else if (shortcut_action.action != Libraries::Ime::ImeKbKeyAction::None) { + kb_state.pressed_action = shortcut_action.action; + keyboard_action_from_hotkey = true; + } + switch (kb_state.pressed_action) { + case Libraries::Ime::ImeKbKeyAction::Character: + consume_temporary_uppercase(insert_text_at_caret(kb_state.pressed_label)); + break; + case Libraries::Ime::ImeKbKeyAction::Shift: + CycleKeyboardCaseState(kb_layout_selection); + break; + case Libraries::Ime::ImeKbKeyAction::SymbolsMode: + ToggleKeyboardFamilyMode(kb_layout_selection, kb_alpha_family, + ImeKbLayoutFamily::Symbols); + break; + case Libraries::Ime::ImeKbKeyAction::SpecialsMode: + ToggleKeyboardFamilyMode(kb_layout_selection, kb_alpha_family, + ImeKbLayoutFamily::Specials); + if (!keyboard_action_from_hotkey && + FocusKeyboardActionKeySelection(kb_layout_selection, ImeKbKeyAction::SpecialsMode, + pending_keyboard_row, pending_keyboard_col)) { + last_keyboard_selected_row = pending_keyboard_row; + last_keyboard_selected_col = pending_keyboard_col; + panel_selection = PanelSelectionTarget::Keyboard; + } + break; + case Libraries::Ime::ImeKbKeyAction::ArrowLeft: + (void)move_text_caret(-1, text_select_mode); + break; + case Libraries::Ime::ImeKbKeyAction::ArrowRight: + (void)move_text_caret(1, text_select_mode); + break; + case Libraries::Ime::ImeKbKeyAction::ArrowUp: + if (!metrics_cfg.multiline) { + // Single-line OSK behavior: up jumps caret to the beginning. + (void)move_text_caret_to_boundary(false, text_select_mode); + } + break; + case Libraries::Ime::ImeKbKeyAction::ArrowDown: + if (!metrics_cfg.multiline) { + // Single-line OSK behavior: down jumps caret to the end. + (void)move_text_caret_to_boundary(true, text_select_mode); + } + break; + case Libraries::Ime::ImeKbKeyAction::PagePrev: + FlipKeyboardModePage(kb_layout_selection, -1); + break; + case Libraries::Ime::ImeKbKeyAction::PageNext: + FlipKeyboardModePage(kb_layout_selection, 1); + break; + case Libraries::Ime::ImeKbKeyAction::Space: + (void)insert_text_at_caret(" "); + break; + case Libraries::Ime::ImeKbKeyAction::Backspace: + (void)backspace_at_caret(); + break; + case Libraries::Ime::ImeKbKeyAction::NewLine: + if (metrics_cfg.multiline) { + (void)insert_text_at_caret("\n"); + } else { + accept_pressed = true; + } + break; + case Libraries::Ime::ImeKbKeyAction::Menu: + if (edit_menu_popup == EditMenuPopup::None) { + open_main_menu(); + opened_menu_this_frame = true; + } else { + edit_menu_popup = EditMenuPopup::None; + menu_activate_armed = true; + } + break; + case Libraries::Ime::ImeKbKeyAction::Settings: + pointer_navigation_active = !pointer_navigation_active; + if (!pointer_navigation_active) { + if (native_input_active) { + native_input_active = false; + ImGui::ClearActiveID(); + } + panel_selection = PanelSelectionTarget::Keyboard; + } + break; + case Libraries::Ime::ImeKbKeyAction::Done: + accept_pressed = true; + break; + default: + break; + } + if (kb_state.done_pressed) { + accept_pressed = true; + } + + if (text_select_mode && edit_menu_popup == EditMenuPopup::None && + !pointer_navigation_active) { + int delta = 0; + if (nav_left || nav_up) { + delta = -1; + } else if (nav_right || nav_down) { + delta = 1; + } + if (delta != 0) { + const int len = text_length_utf16(); + if (text_select_anchor_utf16 < 0 || text_select_focus_utf16 < 0) { + const int caret = std::clamp(state->caret_index, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + } + const int old_focus = std::clamp(text_select_focus_utf16, 0, len); + text_select_focus_utf16 = std::clamp(text_select_focus_utf16 + delta, 0, len); + apply_selection_state(); + emit_update_caret_events(old_focus, text_select_focus_utf16); + } else if (panel_activate_pressed) { + open_actions_menu(); + opened_menu_this_frame = true; + } + } + + if (CloseOskEditMenuOnCancel(edit_menu_popup, cancel_pressed, menu_activate_armed)) { + // Popup closed. + } else if (text_select_mode && cancel_pressed) { + cancel_pressed = false; + text_select_mode = false; + const int len = text_length_utf16(); + const int caret = std::clamp(text_select_focus_utf16, 0, len); + text_select_anchor_utf16 = caret; + text_select_focus_utf16 = caret; + apply_selection_state(); + } + + if (edit_menu_popup != EditMenuPopup::None) { + const bool clipboard_ready = has_clipboard_text(); + const auto previous_popup = edit_menu_popup; + (void)DrawAndHandleOskEditMenuPopup( + edit_menu_popup, edit_menu_index, metrics, draw, pointer_navigation_active, nav_up, + nav_down, cross_down, panel_activate_pressed, opened_menu_this_frame, + menu_activate_armed, clipboard_ready, 1000, "##ImeEditMenuItem", true, + [&](const EditMenuPopup source_popup, const int action_index) { + if (source_popup == EditMenuPopup::Main) { + apply_main_menu_action(action_index); + } else { + apply_actions_menu_action(action_index); + } + }); + if (edit_menu_popup != EditMenuPopup::None && edit_menu_popup != previous_popup) { + opened_menu_this_frame = true; + } + } + + Dummy({metrics.kb_size.x, metrics.kb_size.y + metrics.padding_bottom}); + + if (accept_pressed) { + state->SendEnterEvent(); + } else if (cancel_pressed) { state->SendCloseEvent(); } + + CommitOskPadInputFrame(panel_input, panel_pad_state); + lock_window_scroll(); + SetWindowFontScale(1.0f); } End(); first_render = false; } -void ImeUi::DrawInputText() { - ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f}; - SetCursorPosX(20.0f); - if (first_render) { +bool ImeUi::DrawInputText(const ImePanelMetrics& metrics, bool pointer_selection_enabled) { + const ImVec2 input_size = metrics.input_size; + SetCursorPos(metrics.input_pos_local); + if (request_input_focus) { + SetKeyboardFocusHere(); + request_input_focus = false; + } + + if (state->caret_dirty && !text_select_mode) { + const char* text = state->current_text.begin(); + const int len_utf16 = + Utf16CountFromUtf8Range(text, text + static_cast(state->current_text.size())); + int caret_utf16 = state->caret_index; + if (caret_utf16 < 0) { + caret_utf16 = 0; + } else if (caret_utf16 > len_utf16) { + caret_utf16 = len_utf16; + } + const int caret_byte = Utf8ByteIndexFromUtf16Index(text, caret_utf16); + state->caret_index = caret_utf16; + state->caret_byte_index = caret_byte; + input_cursor_utf16 = caret_utf16; + input_cursor_byte = caret_byte; + input_selection_start_byte = caret_byte; + input_selection_end_byte = caret_byte; + text_select_anchor_utf16 = caret_utf16; + text_select_focus_utf16 = caret_utf16; + if (native_input_active) { + pending_input_selection_apply = true; + request_input_focus = true; + } else { + pending_input_selection_apply = false; + request_input_focus = false; + state->caret_dirty = false; + } + } + + const ImVec2 rect_min = metrics.input_pos_screen; + const ImVec2 rect_max{rect_min.x + input_size.x, rect_min.y + input_size.y}; + const bool clicked_input = IsMouseClicked(ImGuiMouseButton_Left, false) && + IsMouseHoveringRect(rect_min, rect_max, false); + if (clicked_input) { + native_input_active = true; SetKeyboardFocusHere(); } - if (InputTextExLimited("##ImeInput", nullptr, state->current_text.begin(), - ime_param->maxTextLength * 4 + 1, input_size, - ImGuiInputTextFlags_CallbackAlways, ime_param->maxTextLength, - InputTextCallback, this)) { + + ImGuiInputTextFlags flags = + ImGuiInputTextFlags_CallbackAlways | ImGuiInputTextFlags_CallbackCharFilter; + if (!native_input_active) { + flags |= ImGuiInputTextFlags_ReadOnly; } + + PushStyleColor(ImGuiCol_FrameBg, ImeColorToImVec4(style_config.color_text_field)); + PushStyleColor(ImGuiCol_FrameBgHovered, ImeColorToImVec4(style_config.color_preedit)); + PushStyleColor(ImGuiCol_FrameBgActive, ImeColorToImVec4(style_config.color_preedit)); + PushStyleColor(ImGuiCol_Text, ImeColorToImVec4(style_config.color_text)); + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (InputTextExLimited("##ImeInput", nullptr, state->current_text.begin(), + state->max_text_length * 4 + 1, input_size, flags, + state->max_text_length, InputTextCallback, this)) { + } + PopItemFlag(); + PopStyleColor(4); + + const ImRect frame_rect = {GetItemRectMin(), GetItemRectMax()}; + if (!IsItemActive()) { + DrawInactiveCaretOverlay(frame_rect, state->current_text.begin(), input_cursor_byte, + input_selection_start_byte, input_selection_end_byte); + } + + const bool hovered = IsItemHovered(); + if (IsMouseClicked(ImGuiMouseButton_Left, false) && !hovered && native_input_active) { + native_input_active = false; + } + return pointer_selection_enabled && (hovered || IsItemActive() || clicked_input); } int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ImeUi* ui = static_cast(data->UserData); ASSERT(ui); - static std::string lastText; + if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) { + if (!ui->state) { + return 1; + } + const int max_utf16 = static_cast(ui->state->max_text_length); + if (RejectInputCharByUtf16Limit(data, max_utf16)) { + return 1; + } + return 0; + } + + static const ImeUi* last_ui = nullptr; + static std::u16string last_text; static int lastCaretPos = -1; - std::string currentText(data->Buf, data->BufTextLen); - if (currentText != lastText) { + int buf_len = std::max(0, data->BufTextLen); + if (!ui->native_input_active) { + ui->request_input_select_all = false; + ui->pending_input_selection_apply = false; + last_ui = nullptr; + last_text.clear(); + lastCaretPos = -1; + const int cursor_byte = std::clamp(ui->input_cursor_byte, 0, buf_len); + const int selection_start = std::clamp(ui->input_selection_start_byte, 0, buf_len); + const int selection_end = std::clamp(ui->input_selection_end_byte, 0, buf_len); + data->CursorPos = cursor_byte; + data->SelectionStart = selection_start; + data->SelectionEnd = selection_end; + return 0; + } + + if (ui->request_input_select_all) { + data->SelectAll(); + ui->request_input_select_all = false; + } + + if (ui->pending_input_selection_apply) { + const int len_chars = Utf16CountFromUtf8Range(data->Buf, data->Buf + buf_len); + int anchor = ui->text_select_anchor_utf16; + int focus = ui->text_select_focus_utf16; + if (anchor < 0 || focus < 0) { + const int caret_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->CursorPos); + anchor = caret_utf16; + focus = caret_utf16; + } + anchor = std::clamp(anchor, 0, len_chars); + focus = std::clamp(focus, 0, len_chars); + const int anchor_byte = Utf8ByteIndexFromUtf16Index(data->Buf, anchor); + const int focus_byte = Utf8ByteIndexFromUtf16Index(data->Buf, focus); + data->CursorPos = focus_byte; + data->SelectionStart = std::min(anchor_byte, focus_byte); + data->SelectionEnd = std::max(anchor_byte, focus_byte); + ui->text_select_anchor_utf16 = anchor; + ui->text_select_focus_utf16 = focus; + ui->pending_input_selection_apply = false; + } + + bool caret_set_from_api = false; + if (ui->state->caret_dirty && !ui->text_select_mode) { + const int len_chars = Utf16CountFromUtf8Range(data->Buf, data->Buf + buf_len); + 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_index = caret; + ui->state->caret_byte_index = caret_byte; + ui->state->caret_dirty = false; + caret_set_from_api = true; + } + + if (ClampInputBufferToUtf16Limit(data, static_cast(ui->state->max_text_length))) { + buf_len = std::max(0, data->BufTextLen); + } + + const int cursor_byte = std::clamp(data->CursorPos, 0, buf_len); + + constexpr std::size_t kImeTextCapacity = ORBIS_IME_MAX_TEXT_LENGTH + 1; + const std::size_t max_orbis_len = std::min( + static_cast(ui->state->max_text_length) + 1, kImeTextCapacity); + + if (last_ui != ui) { + last_ui = ui; + std::array snapshot{}; + if (ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, snapshot.data(), + max_orbis_len)) { + last_text.assign(snapshot.data()); + } else { + last_text.clear(); + } + lastCaretPos = cursor_byte; + } + + std::array current_text_u16{}; + if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, current_text_u16.data(), + max_orbis_len)) { + LOG_ERROR(Lib_Ime, "Failed to convert UTF-8 to Orbis for current text"); + return 0; + } + std::u16string current_text(current_text_u16.data()); + + if (current_text != last_text) { OrbisImeEditText eventParam{}; eventParam.str = reinterpret_cast(ui->ime_param->work); eventParam.area_num = 1; @@ -305,10 +1702,26 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { return 0; } - eventParam.caret_index = data->CursorPos; - eventParam.text_area[0].index = data->CursorPos; - eventParam.text_area[0].length = - (data->CursorPos > lastCaretPos) ? 1 : -1; // data->CursorPos; + std::size_t prefix = 0; + while (prefix < last_text.size() && prefix < current_text.size() && + last_text[prefix] == current_text[prefix]) { + ++prefix; + } + + std::size_t old_tail = last_text.size(); + std::size_t new_tail = current_text.size(); + while (old_tail > prefix && new_tail > prefix && + last_text[old_tail - 1] == current_text[new_tail - 1]) { + --old_tail; + --new_tail; + } + + const s32 removed = static_cast(old_tail - prefix); + const s32 inserted = static_cast(new_tail - prefix); + eventParam.caret_index = + static_cast(Utf16CountFromUtf8Range(data->Buf, data->Buf + cursor_byte)); + eventParam.text_area[0].index = static_cast(prefix); + eventParam.text_area[0].length = inserted - removed; OrbisImeEvent event{}; event.id = OrbisImeEventId::UpdateText; @@ -321,32 +1734,55 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { static_cast(eventParam.text_area[0].mode), eventParam.text_area[0].index, eventParam.text_area[0].length); - lastText = currentText; - lastCaretPos = -1; + last_text = current_text; + lastCaretPos = cursor_byte; ui->state->SendEvent(&event); } if (lastCaretPos == -1) { - lastCaretPos = data->CursorPos; - } else if (data->CursorPos != lastCaretPos) { - const int delta = data->CursorPos - lastCaretPos; + lastCaretPos = cursor_byte; + } else if (cursor_byte != lastCaretPos) { + const int old_cursor_byte = std::clamp(lastCaretPos, 0, buf_len); + const int old_cursor_utf16 = + Utf16CountFromUtf8Range(data->Buf, data->Buf + old_cursor_byte); + const int new_cursor_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + cursor_byte); + const int delta = new_cursor_utf16 - old_cursor_utf16; - // Emit one UpdateCaret per delta step (delta may be ±1 or a jump) - const bool move_right = delta > 0; - const u32 steps = static_cast(std::abs(delta)); - OrbisImeCaretMovementDirection dir = move_right ? OrbisImeCaretMovementDirection::Right - : OrbisImeCaretMovementDirection::Left; + if (delta != 0 && !caret_set_from_api) { + // Emit one UpdateCaret per UTF-16 unit step (delta may be ±1 or a jump). + const bool move_right = delta > 0; + const u32 steps = static_cast(std::abs(delta)); + OrbisImeCaretMovementDirection dir = move_right ? OrbisImeCaretMovementDirection::Right + : OrbisImeCaretMovementDirection::Left; - for (u32 i = 0; i < steps; ++i) { - OrbisImeEvent caret_step{}; - caret_step.id = OrbisImeEventId::UpdateCaret; - caret_step.param.caret_move = dir; - LOG_DEBUG(Lib_Ime, "IME Event queued: UpdateCaret(step {}/{}), dir={}", i + 1, steps, - static_cast(dir)); - ui->state->SendEvent(&caret_step); + for (u32 i = 0; i < steps; ++i) { + OrbisImeEvent caret_step{}; + caret_step.id = OrbisImeEventId::UpdateCaret; + caret_step.param.caret_move = dir; + LOG_DEBUG(Lib_Ime, "IME Event queued: UpdateCaret(step {}/{}), dir={}", i + 1, + steps, static_cast(dir)); + ui->state->SendEvent(&caret_step); + } } - lastCaretPos = data->CursorPos; + lastCaretPos = cursor_byte; + } + + const int selection_start_byte = + std::clamp(std::min(data->SelectionStart, data->SelectionEnd), 0, buf_len); + const int selection_end_byte = + std::clamp(std::max(data->SelectionStart, data->SelectionEnd), 0, buf_len); + ui->input_cursor_byte = cursor_byte; + ui->input_cursor_utf16 = Utf16CountFromUtf8Range(data->Buf, data->Buf + cursor_byte); + ui->input_selection_start_byte = selection_start_byte; + ui->input_selection_end_byte = selection_end_byte; + ui->state->caret_byte_index = cursor_byte; + ui->state->caret_index = ui->input_cursor_utf16; + ui->state->caret_dirty = false; + + if (!ui->text_select_mode) { + ui->text_select_anchor_utf16 = ui->input_cursor_utf16; + ui->text_select_focus_utf16 = ui->input_cursor_utf16; } return 0; @@ -354,6 +1790,10 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { void ImeUi::Free() { RemoveLayer(this); + if (gamepad_input_capture_active) { + ImGui::Core::ReleaseGamepadInputCapture(); + gamepad_input_capture_active = false; + } } }; // namespace Libraries::Ime diff --git a/src/core/libraries/ime/ime_ui.h b/src/core/libraries/ime/ime_ui.h index 5eb371bd9..5f65b1cda 100644 --- a/src/core/libraries/ime/ime_ui.h +++ b/src/core/libraries/ime/ime_ui.h @@ -11,12 +11,14 @@ #include "common/cstring.h" #include "common/types.h" +#include "core/libraries/ime/ime_kb_layout.h" #include "ime.h" namespace Libraries::Ime { class ImeHandler; class ImeUi; +struct ImePanelMetrics; class ImeState { friend class ImeHandler; @@ -28,6 +30,9 @@ class ImeState { // A character can hold up to 4 bytes in UTF-8 Common::CString current_text; + int caret_index = 0; + int caret_byte_index = 0; + bool caret_dirty = false; std::queue event_queue; std::mutex queue_mutex; @@ -52,11 +57,81 @@ private: }; class ImeUi : public ImGui::Layer { + enum class PanelSelectionTarget : u8 { + Input = 0, + Prediction = 1, + Close = 2, + Keyboard = 3, + }; + + enum class EditMenuPopup : u8 { + None = 0, + Main = 1, + Actions = 2, + }; + ImeState* state{}; const OrbisImeParam* ime_param{}; const OrbisImeParamExtended* extended_param{}; + ImeStyleConfig style_config{}; bool first_render = true; + bool accept_armed = false; + bool native_input_active = false; + bool pointer_navigation_active = true; + EditMenuPopup edit_menu_popup = EditMenuPopup::None; + bool menu_activate_armed = true; + bool l2_shortcut_armed = true; + bool request_input_focus = false; + bool request_input_select_all = false; + bool text_select_mode = false; + bool pending_input_selection_apply = false; + bool prev_virtual_cross_down = false; + bool prev_virtual_lstick_left_down = false; + bool prev_virtual_lstick_right_down = false; + bool prev_virtual_lstick_up_down = false; + bool prev_virtual_lstick_down_down = false; + int left_stick_repeat_dir = 0; + double left_stick_next_repeat_time = 0.0; + double virtual_cross_next_repeat_time = 0.0; + double virtual_triangle_next_repeat_time = 0.0; + u32 prev_virtual_buttons = 0; + bool prev_virtual_square_down = false; + bool prev_virtual_l1_down = false; + bool prev_virtual_r1_down = false; + bool prev_virtual_dpad_left_down = false; + bool prev_virtual_dpad_right_down = false; + bool prev_virtual_dpad_up_down = false; + bool prev_virtual_dpad_down_down = false; + double virtual_square_next_repeat_time = 0.0; + double virtual_l1_next_repeat_time = 0.0; + double virtual_r1_next_repeat_time = 0.0; + double virtual_dpad_left_next_repeat_time = 0.0; + double virtual_dpad_right_next_repeat_time = 0.0; + double virtual_dpad_up_next_repeat_time = 0.0; + double virtual_dpad_down_next_repeat_time = 0.0; + ImeEdgeWrapNavState panel_vertical_nav_state{}; + bool panel_position_initialized = false; + bool panel_drag_active = false; + bool gamepad_input_capture_active = false; + ImVec2 panel_position{}; + int input_cursor_utf16 = 0; + int input_cursor_byte = 0; + int input_selection_start_byte = 0; + int input_selection_end_byte = 0; + int text_select_anchor_utf16 = -1; + int text_select_focus_utf16 = -1; + int top_virtual_col = 0; + PanelSelectionTarget panel_selection = PanelSelectionTarget::Keyboard; + int pending_keyboard_row = -1; + int pending_keyboard_col = -1; + int last_keyboard_selected_row = 0; + int last_keyboard_selected_col = 0; + int edit_menu_index = 0; + ImeKbLayoutSelection kb_layout_selection{}; + ImeKbLayoutSelection last_nav_layout_selection{}; + bool nav_layout_selection_initialized = false; + ImeKbLayoutFamily kb_alpha_family = ImeKbLayoutFamily::Latin; std::mutex draw_mutex; public: @@ -71,9 +146,9 @@ public: private: void Free(); - void DrawInputText(); + bool DrawInputText(const ImePanelMetrics& metrics, bool pointer_selection_enabled); static int InputTextCallback(ImGuiInputTextCallbackData* data); }; -}; // namespace Libraries::Ime \ No newline at end of file +}; // namespace Libraries::Ime diff --git a/src/core/libraries/ime/ime_ui_shared.cpp b/src/core/libraries/ime/ime_ui_shared.cpp new file mode 100644 index 000000000..45dbbfe26 --- /dev/null +++ b/src/core/libraries/ime/ime_ui_shared.cpp @@ -0,0 +1,1098 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/ime/ime_ui_shared.h" + +#include +#include +#include + +#include "common/singleton.h" +#include "input/controller.h" + +namespace Libraries::Ime { + +namespace { + +enum class StickNavDirection : int { + None = 0, + Left = 1, + Right = 2, + Up = 3, + Down = 4, +}; + +constexpr float kNavAxisThreshold = 0.50f; +constexpr double kStickNavInitialDelaySlow = 0.26; +constexpr double kStickNavInitialDelayFast = 0.16; +constexpr double kStickNavRepeatSlow = 0.20; +constexpr double kStickNavRepeatFast = 0.11; +constexpr double kCaretBlinkCycleSec = 1.20; +constexpr double kCaretBlinkOnSec = 0.80; + +struct VirtualButtonRepeatResult { + bool pressed = false; + bool repeat = false; +}; + +struct StickNavPulseState { + int repeat_dir = 0; + double next_repeat_time = 0.0; +}; + +struct StickNavPulseResult { + bool left = false; + bool right = false; + bool up = false; + bool down = false; + bool left_repeat = false; + bool right_repeat = false; + bool up_repeat = false; + bool down_repeat = false; +}; + +bool IsCaretBlinkVisible() { + const ImGuiIO& io = ImGui::GetIO(); + if (!io.ConfigInputTextCursorBlink) { + return true; + } + const double phase = std::fmod(ImGui::GetTime(), kCaretBlinkCycleSec); + return phase <= kCaretBlinkOnSec; +} + +} // namespace + +namespace UiShared { + +constexpr float kAxisDeadzone = 0.24f; +constexpr float kPanelMoveSpeed = 900.0f; // pixels per second at full tilt + +float ToAxisUnit(const s32 axis) { + const float centered = (static_cast(axis) - 128.0f) / 127.0f; + return std::clamp(centered, -1.0f, 1.0f); +} + +bool ReadControllerState(Libraries::UserService::OrbisUserServiceUserId user_id, + Input::State* out_state) { + if (!out_state) { + return false; + } + auto* controllers = Common::Singleton::Instance(); + if (!controllers) { + return false; + } + + const auto read_state = [&](u8 index, bool* has_state) { + if (has_state) { + *has_state = false; + } + Input::State pad_state{}; + bool connected = false; + int connected_count = 0; + (*controllers)[index]->ReadState(&pad_state, &connected, &connected_count); + if (!connected) { + return false; + } + if (has_state) { + *has_state = true; + } + *out_state = pad_state; + return true; + }; + + const auto mapped = Input::GameControllers::GetControllerIndexFromUserID(user_id); + if (mapped.has_value() && *mapped < 5) { + bool has_state = false; + if (read_state(*mapped, &has_state) && has_state) { + return true; + } + } + + for (u8 index = 0; index < 5; ++index) { + if (mapped.has_value() && index == *mapped) { + continue; + } + bool has_state = false; + if (read_state(index, &has_state) && has_state) { + return true; + } + } + return false; +} + +} // namespace UiShared + +ImVec4 BrightenColor(ImU32 color, float delta) { + ImVec4 out = ImGui::ColorConvertU32ToFloat4(color); + out.x = std::clamp(out.x + delta, 0.0f, 1.0f); + out.y = std::clamp(out.y + delta, 0.0f, 1.0f); + out.z = std::clamp(out.z + delta, 0.0f, 1.0f); + return out; +} + +static ImU32 ScaleColorAlpha(ImU32 color, float alpha_scale) { + ImVec4 c = ImGui::ColorConvertU32ToFloat4(color); + c.w = std::clamp(c.w * alpha_scale, 0.0f, 1.0f); + return ImGui::ColorConvertFloat4ToU32(c); +} + +void TriggerSelectorPressPulse(SelectorFadeState& state, double now) { + state.press_pulse_started_at = now; +} + +float ComputePressPulseExpand(double pulse_started_at, double now, float pulse_duration_sec, + float max_expand_px) { + if (pulse_started_at < 0.0 || pulse_duration_sec <= 0.0f || max_expand_px <= 0.0f) { + return 0.0f; + } + const double elapsed = now - pulse_started_at; + if (elapsed < 0.0 || elapsed >= static_cast(pulse_duration_sec)) { + return 0.0f; + } + + const float t = static_cast(elapsed / static_cast(pulse_duration_sec)); + const float envelope = std::sin(t * std::numbers::pi_v); + return std::max(0.0f, envelope) * max_expand_px; +} + +void UpdateSelectorFadeState(SelectorFadeState& state, ImVec2 pos, ImVec2 size, float inset, + float corner_radius, bool selected, double now) { + bool next_visible = false; + ImVec2 next_min{}; + ImVec2 next_max{}; + if (selected && size.x > inset * 2.0f && size.y > inset * 2.0f) { + next_visible = true; + next_min = {pos.x + inset, pos.y + inset}; + next_max = {pos.x + size.x - inset, pos.y + size.y - inset}; + } + + const bool was_visible = state.current_visible; + if (was_visible && !next_visible) { + state.previous_min = state.current_min; + state.previous_max = state.current_max; + state.previous_corner_radius = state.current_corner_radius; + state.previous_visible = true; + state.previous_started_at = now; + } + + state.current_visible = next_visible; + if (!next_visible) { + return; + } + state.current_min = next_min; + state.current_max = next_max; + state.current_corner_radius = std::max(0.0f, corner_radius); +} + +void DrawSelectorFadeState(const SelectorFadeState& state, ImDrawList* draw_list, + ImU32 overlay_color, ImU32 border_color, float border_thickness, + float fade_duration_sec, double now, float current_expand_px) { + if (!draw_list) { + return; + } + + if (state.previous_visible && fade_duration_sec > 0.0f) { + const double elapsed = now - state.previous_started_at; + if (elapsed >= 0.0 && elapsed < static_cast(fade_duration_sec)) { + const float alpha = + 1.0f - static_cast(elapsed / static_cast(fade_duration_sec)); + if (alpha > 0.0f) { + draw_list->AddRectFilled(state.previous_min, state.previous_max, + ScaleColorAlpha(overlay_color, alpha), + state.previous_corner_radius); + draw_list->AddRect(state.previous_min, state.previous_max, + ScaleColorAlpha(border_color, alpha), + state.previous_corner_radius, 0, border_thickness); + } + } + } + + if (!state.current_visible) { + return; + } + const float expand = std::max(0.0f, current_expand_px); + const ImVec2 current_min{state.current_min.x - expand, state.current_min.y - expand}; + const ImVec2 current_max{state.current_max.x + expand, state.current_max.y + expand}; + const float corner_radius = state.current_corner_radius + expand; + draw_list->AddRectFilled(current_min, current_max, overlay_color, corner_radius); + draw_list->AddRect(current_min, current_max, border_color, corner_radius, 0, border_thickness); +} + +ImeKbLayoutSelection ResolveInitialKbLayoutSelection(OrbisImeExtOption ext_option, + OrbisImePanelPriority panel_priority) { + ImeKbLayoutSelection selection{}; + const bool set_priority = True(ext_option & OrbisImeExtOption::SET_PRIORITY); + if (set_priority) { + switch (panel_priority) { + case OrbisImePanelPriority::Symbol: + selection.family = ImeKbLayoutFamily::Symbols; + break; + case OrbisImePanelPriority::Accent: + selection.family = ImeKbLayoutFamily::Specials; + break; + case OrbisImePanelPriority::Alphabet: + case OrbisImePanelPriority::Default: + default: + selection.family = ImeKbLayoutFamily::Latin; + break; + } + } + + const bool shift_lock = set_priority && True(ext_option & OrbisImeExtOption::PRIORITY_SHIFT); + if (shift_lock && selection.family != ImeKbLayoutFamily::Symbols) { + // SDK docs: PRIORITY_SHIFT starts the initial panel in Shift-lock state. + selection.case_state = ImeKbCaseState::CapsLock; + } + return selection; +} + +void InitializeDefaultOskSelectionAnchor(const ImeKbLayoutSelection& layout_selection, + OrbisImeExtOption ext_option, int& pending_row, + int& pending_col, int& last_row, int& last_col) { + constexpr int kLatinLowerDefaultRow = 2; + constexpr int kLatinLowerDefaultCol = 4; + + const bool is_default_latin_lower = layout_selection.family == ImeKbLayoutFamily::Latin && + layout_selection.case_state == ImeKbCaseState::Lower; + const bool game_requested_other = + True(ext_option & OrbisImeExtOption::SET_PRIORITY) && !is_default_latin_lower; + if (!is_default_latin_lower || game_requested_other) { + return; + } + + pending_row = kLatinLowerDefaultRow; + pending_col = kLatinLowerDefaultCol; + last_row = kLatinLowerDefaultRow; + last_col = kLatinLowerDefaultCol; +} + +void CycleKeyboardCaseState(ImeKbLayoutSelection& selection) { + switch (selection.case_state) { + case ImeKbCaseState::Lower: + selection.case_state = ImeKbCaseState::Upper; + break; + case ImeKbCaseState::Upper: + selection.case_state = ImeKbCaseState::CapsLock; + break; + case ImeKbCaseState::CapsLock: + default: + selection.case_state = ImeKbCaseState::Lower; + break; + } +} + +void ToggleKeyboardFamilyMode(ImeKbLayoutSelection& selection, ImeKbLayoutFamily& alpha_family, + ImeKbLayoutFamily target_family) { + const auto set_family_and_reset_page = [&](const ImeKbLayoutFamily family) { + selection.family = family; + selection.page = 0; + if (family == ImeKbLayoutFamily::Latin || family == ImeKbLayoutFamily::Specials) { + alpha_family = family; + } + }; + + if (target_family == ImeKbLayoutFamily::Symbols) { + if (selection.family == ImeKbLayoutFamily::Symbols) { + set_family_and_reset_page(alpha_family); + } else { + if (selection.family == ImeKbLayoutFamily::Latin || + selection.family == ImeKbLayoutFamily::Specials) { + alpha_family = selection.family; + } + set_family_and_reset_page(ImeKbLayoutFamily::Symbols); + } + return; + } + + if (selection.family == target_family) { + set_family_and_reset_page(ImeKbLayoutFamily::Latin); + } else { + set_family_and_reset_page(target_family); + } +} + +bool FocusKeyboardActionKeySelection(const ImeKbLayoutSelection& selection, ImeKbKeyAction action, + int& out_row, int& out_col) { + const auto& layout = GetImeKeyboardLayout(selection); + if (!layout.keys || layout.key_count == 0) { + return false; + } + + for (std::size_t i = 0; i < layout.key_count; ++i) { + const auto& key = layout.keys[i]; + if (key.action != action) { + continue; + } + out_row = static_cast(key.row); + out_col = static_cast(key.col); + return true; + } + return false; +} + +void FlipKeyboardModePage(ImeKbLayoutSelection& selection, int direction) { + if (selection.family != ImeKbLayoutFamily::Symbols && + selection.family != ImeKbLayoutFamily::Specials) { + return; + } + const int page = static_cast(selection.page); + selection.page = static_cast((page + direction + 2) % 2); +} + +int Utf16CountFromUtf8Range(const char* text, const char* end) { + if (!text) { + return 0; + } + const char* const range_end = end ? end : (text + std::strlen(text)); + const char* p = text; + int utf16_units = 0; + while (p < range_end && *p) { + unsigned int c = 0; + const int step = ImTextCharFromUtf8(&c, p, range_end); + if (step <= 0) { + break; + } + utf16_units += (c > 0xFFFF) ? 2 : 1; + p += step; + } + return utf16_units; +} + +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); +} + +bool RejectInputCharByUtf16Limit(const ImGuiInputTextCallbackData* data, int max_utf16) { + if (!data || max_utf16 < 0 || data->EventChar == 0) { + return false; + } + ImGuiContext* g = data->Ctx; + if (!g) { + return false; + } + ImGuiInputTextState* st = &g->InputTextState; + if (!st || !st->TextSrc) { + return false; + } + + const int cur_units = Utf16CountFromUtf8Range(st->TextSrc, st->TextSrc + st->TextLen); + int selected_units = 0; + if (st->HasSelection()) { + const int sel_begin = st->GetSelectionStart(); + const int sel_end = st->GetSelectionEnd(); + selected_units = Utf16CountFromUtf8Range(st->TextSrc + sel_begin, st->TextSrc + sel_end); + } + + const int incoming_units = data->EventChar > 0xFFFF ? 2 : 1; + const int remaining_units = max_utf16 - (cur_units - selected_units); + return remaining_units < incoming_units; +} + +bool ClampInputBufferToUtf16Limit(ImGuiInputTextCallbackData* data, int max_utf16) { + if (!data || max_utf16 < 0) { + return false; + } + const int utf16_len = Utf16CountFromUtf8Range(data->Buf, data->Buf + data->BufTextLen); + if (utf16_len <= max_utf16) { + return false; + } + + const int keep_bytes = Utf8ByteIndexFromUtf16Index(data->Buf, max_utf16); + if (keep_bytes < data->BufTextLen) { + data->DeleteChars(keep_bytes, data->BufTextLen - keep_bytes); + } + data->CursorPos = std::clamp(data->CursorPos, 0, data->BufTextLen); + data->SelectionStart = std::clamp(data->SelectionStart, 0, data->BufTextLen); + data->SelectionEnd = std::clamp(data->SelectionEnd, 0, data->BufTextLen); + return true; +} + +void DrawInactiveCaretOverlay(const ImRect& frame_rect, const char* text, int caret_byte, + int selection_start_byte, int selection_end_byte, bool multiline) { + if (!text) { + return; + } + if (selection_start_byte != selection_end_byte) { + return; + } + if (!IsCaretBlinkVisible()) { + return; + } + + const int text_len = static_cast(std::strlen(text)); + const int clamped_byte = std::clamp(caret_byte, 0, text_len); + const char* const caret_ptr = text + clamped_byte; + + const ImGuiStyle& style = ImGui::GetStyle(); + const float left = frame_rect.Min.x + style.FramePadding.x; + const float right = frame_rect.Max.x - style.FramePadding.x; + if (right <= left) { + return; + } + + const float top = frame_rect.Min.y + style.FramePadding.y; + const float bottom = frame_rect.Max.y - style.FramePadding.y; + if (bottom <= top) { + return; + } + + float caret_x = left; + float caret_y = top; + float caret_bottom = bottom; + if (multiline) { + const char* line_begin = text; + int line_index = 0; + const char* p = text; + while (p < caret_ptr) { + if (*p == '\n') { + line_begin = p + 1; + ++line_index; + } + ++p; + } + caret_x = left + ImGui::CalcTextSize(line_begin, caret_ptr, false, -1.0f).x; + caret_y = top + static_cast(line_index) * ImGui::GetTextLineHeight(); + caret_bottom = caret_y + ImGui::GetTextLineHeight(); + } else { + caret_x = left + ImGui::CalcTextSize(text, caret_ptr, false, -1.0f).x; + } + + caret_x = std::clamp(caret_x, left, right); + caret_y = std::clamp(caret_y, top, bottom); + caret_bottom = std::clamp(caret_bottom, caret_y, bottom); + if (caret_bottom <= caret_y) { + return; + } + + const ImU32 caret_col = ImGui::GetColorU32(ImGuiCol_Text); + ImGui::GetWindowDrawList()->AddLine({caret_x, caret_y}, {caret_x, caret_bottom}, caret_col, + 1.5f); +} + +static float ApplyAxisDeadzone(float v) { + const float av = std::abs(v); + if (av <= UiShared::kAxisDeadzone) { + return 0.0f; + } + const float scaled = (av - UiShared::kAxisDeadzone) / (1.0f - UiShared::kAxisDeadzone); + return std::copysign(std::clamp(scaled, 0.0f, 1.0f), v); +} + +VirtualPadSnapshot ReadVirtualPadSnapshot(Libraries::UserService::OrbisUserServiceUserId user_id, + float delta_time, bool include_imgui_fallback) { + VirtualPadSnapshot snapshot{}; + constexpr u32 kMaskLeft = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Left); + constexpr u32 kMaskRight = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Right); + constexpr u32 kMaskUp = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Up); + constexpr u32 kMaskDown = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Down); + constexpr u32 kMaskCross = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Cross); + constexpr u32 kMaskTriangle = + static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Triangle); + constexpr u32 kMaskSquare = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Square); + constexpr u32 kMaskCircle = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Circle); + constexpr u32 kMaskL1 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L1); + constexpr u32 kMaskR1 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R1); + constexpr u32 kMaskL2 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L2); + constexpr u32 kMaskR2 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R2); + constexpr u32 kMaskL3 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L3); + constexpr u32 kMaskR3 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R3); + constexpr float kTriggerButtonThreshold = 0.25f; + + float lx = 0.0f; + float ly = 0.0f; + float rx = 0.0f; + float ry = 0.0f; + float l2_analog = 0.0f; + float r2_analog = 0.0f; + u32 buttons = 0; + Input::State state{}; + if (UiShared::ReadControllerState(user_id, &state)) { + buttons = static_cast(state.buttonsState); + lx = ApplyAxisDeadzone( + UiShared::ToAxisUnit(state.axes[static_cast(Input::Axis::LeftX)])); + ly = ApplyAxisDeadzone( + UiShared::ToAxisUnit(state.axes[static_cast(Input::Axis::LeftY)])); + rx = ApplyAxisDeadzone( + UiShared::ToAxisUnit(state.axes[static_cast(Input::Axis::RightX)])); + ry = ApplyAxisDeadzone( + UiShared::ToAxisUnit(state.axes[static_cast(Input::Axis::RightY)])); + l2_analog = std::clamp( + static_cast(state.axes[static_cast(Input::Axis::TriggerLeft)]) / + 255.0f, + 0.0f, 1.0f); + r2_analog = std::clamp( + static_cast(state.axes[static_cast(Input::Axis::TriggerRight)]) / + 255.0f, + 0.0f, 1.0f); + } + + if (include_imgui_fallback) { + const float imgui_lx = + ApplyAxisDeadzone(ImGui::GetKeyData(ImGuiKey_GamepadLStickRight)->AnalogValue - + ImGui::GetKeyData(ImGuiKey_GamepadLStickLeft)->AnalogValue); + const float imgui_ly = + ApplyAxisDeadzone(ImGui::GetKeyData(ImGuiKey_GamepadLStickDown)->AnalogValue - + ImGui::GetKeyData(ImGuiKey_GamepadLStickUp)->AnalogValue); + const float imgui_rx = + ApplyAxisDeadzone(ImGui::GetKeyData(ImGuiKey_GamepadRStickRight)->AnalogValue - + ImGui::GetKeyData(ImGuiKey_GamepadRStickLeft)->AnalogValue); + const float imgui_ry = + ApplyAxisDeadzone(ImGui::GetKeyData(ImGuiKey_GamepadRStickDown)->AnalogValue - + ImGui::GetKeyData(ImGuiKey_GamepadRStickUp)->AnalogValue); + const float imgui_l2 = + std::clamp(ImGui::GetKeyData(ImGuiKey_GamepadL2)->AnalogValue, 0.0f, 1.0f); + const float imgui_r2 = + std::clamp(ImGui::GetKeyData(ImGuiKey_GamepadR2)->AnalogValue, 0.0f, 1.0f); + + if (std::abs(imgui_lx) > std::abs(lx)) { + lx = imgui_lx; + } + if (std::abs(imgui_ly) > std::abs(ly)) { + ly = imgui_ly; + } + if (std::abs(imgui_rx) > std::abs(rx)) { + rx = imgui_rx; + } + if (std::abs(imgui_ry) > std::abs(ry)) { + ry = imgui_ry; + } + l2_analog = std::max(l2_analog, imgui_l2); + r2_analog = std::max(r2_analog, imgui_r2); + + const auto merge_imgui_button = [&](const ImGuiKey key, const u32 mask) { + if (ImGui::IsKeyDown(key)) { + buttons |= mask; + } + }; + merge_imgui_button(ImGuiKey_GamepadDpadLeft, kMaskLeft); + merge_imgui_button(ImGuiKey_GamepadDpadRight, kMaskRight); + merge_imgui_button(ImGuiKey_GamepadDpadUp, kMaskUp); + merge_imgui_button(ImGuiKey_GamepadDpadDown, kMaskDown); + merge_imgui_button(ImGuiKey_GamepadFaceDown, kMaskCross); + merge_imgui_button(ImGuiKey_GamepadFaceUp, kMaskTriangle); + merge_imgui_button(ImGuiKey_GamepadFaceLeft, kMaskSquare); + merge_imgui_button(ImGuiKey_GamepadFaceRight, kMaskCircle); + merge_imgui_button(ImGuiKey_GamepadL1, kMaskL1); + merge_imgui_button(ImGuiKey_GamepadR1, kMaskR1); + merge_imgui_button(ImGuiKey_GamepadL3, kMaskL3); + merge_imgui_button(ImGuiKey_GamepadR3, kMaskR3); + if (l2_analog >= kTriggerButtonThreshold) { + buttons |= kMaskL2; + } + if (r2_analog >= kTriggerButtonThreshold) { + buttons |= kMaskR2; + } + } + + snapshot.buttons = buttons; + snapshot.left_stick = {lx, ly}; + snapshot.l2_analog = l2_analog; + + if (delta_time > 0.0f) { + snapshot.panel_delta = {rx * UiShared::kPanelMoveSpeed * delta_time, + ry * UiShared::kPanelMoveSpeed * delta_time}; + } + return snapshot; +} + +static StickNavDirection ResolveStickNavDirection(float lx, float ly, float* strength) { + const float abs_x = std::abs(lx); + const float abs_y = std::abs(ly); + if (abs_x < kNavAxisThreshold && abs_y < kNavAxisThreshold) { + if (strength) { + *strength = 0.0f; + } + return StickNavDirection::None; + } + + if (abs_x >= abs_y) { + if (strength) { + *strength = abs_x; + } + return (lx < 0.0f) ? StickNavDirection::Left : StickNavDirection::Right; + } + + if (strength) { + *strength = abs_y; + } + return (ly < 0.0f) ? StickNavDirection::Up : StickNavDirection::Down; +} + +static VirtualButtonRepeatResult ProcessRepeatButton(bool down, bool edge_pressed, double now, + double repeat_delay, double repeat_rate, + bool& prev_down_state, + double& next_repeat_time) { + VirtualButtonRepeatResult result{}; + if (!down) { + prev_down_state = false; + next_repeat_time = 0.0; + return result; + } + + if (!prev_down_state) { + prev_down_state = true; + next_repeat_time = now + repeat_delay; + result.pressed = edge_pressed; + return result; + } + + if (edge_pressed) { + next_repeat_time = now + repeat_delay; + result.pressed = true; + return result; + } + + if (repeat_rate <= 0.0) { + return result; + } + + if (now >= next_repeat_time) { + next_repeat_time = now + repeat_rate; + result.pressed = true; + result.repeat = true; + } + return result; +} + +static bool IsStickDirectionEdgePressed(StickNavDirection direction, bool left_edge, + bool right_edge, bool up_edge, bool down_edge) { + switch (direction) { + case StickNavDirection::Left: + return left_edge; + case StickNavDirection::Right: + return right_edge; + case StickNavDirection::Up: + return up_edge; + case StickNavDirection::Down: + return down_edge; + case StickNavDirection::None: + default: + return false; + } +} + +static StickNavPulseResult ProcessStickNavPulse(bool enabled, StickNavDirection direction, + float strength, bool direction_edge_pressed, + StickNavPulseState& state, double now) { + StickNavPulseResult result{}; + if (!enabled || direction == StickNavDirection::None) { + state.repeat_dir = 0; + state.next_repeat_time = 0.0; + return result; + } + + const float t = + std::clamp((strength - kNavAxisThreshold) / (1.0f - kNavAxisThreshold), 0.0f, 1.0f); + const double initial_delay = + std::lerp(kStickNavInitialDelaySlow, kStickNavInitialDelayFast, static_cast(t)); + const double repeat_interval = + std::lerp(kStickNavRepeatSlow, kStickNavRepeatFast, static_cast(t)); + + const int dir_id = static_cast(direction); + const int prev_dir = state.repeat_dir; + bool pulse = false; + bool pulse_repeat = false; + + if (prev_dir != dir_id) { + // Treat direction changes before the pending repeat tick as fast-rotate input. + const bool fast_rotate_change = (prev_dir != 0) && (now < state.next_repeat_time); + state.repeat_dir = dir_id; + state.next_repeat_time = now + initial_delay; + pulse = (prev_dir == 0) || direction_edge_pressed || fast_rotate_change; + } else if (direction_edge_pressed) { + pulse = true; + state.next_repeat_time = now + initial_delay; + } else if (now >= state.next_repeat_time) { + pulse = true; + pulse_repeat = true; + state.next_repeat_time = now + repeat_interval; + } + + if (!pulse) { + return result; + } + + switch (direction) { + case StickNavDirection::Left: + result.left = true; + result.left_repeat = pulse_repeat; + break; + case StickNavDirection::Right: + result.right = true; + result.right_repeat = pulse_repeat; + break; + case StickNavDirection::Up: + result.up = true; + result.up_repeat = pulse_repeat; + break; + case StickNavDirection::Down: + result.down = true; + result.down_repeat = pulse_repeat; + break; + case StickNavDirection::None: + default: + break; + } + return result; +} + +OskVirtualPadInputView::OskVirtualPadInputView(const OskPadInputFrame& input_frame, + const ImGuiIO& io) + : frame(input_frame), repeat_delay(static_cast(io.KeyRepeatDelay)), + repeat_rate(static_cast(io.KeyRepeatRate)) {} + +bool OskVirtualPadInputView::Down(Libraries::Pad::OrbisPadButtonDataOffset button) const { + return (frame.virtual_buttons & static_cast(button)) != 0; +} + +bool OskVirtualPadInputView::Pressed(Libraries::Pad::OrbisPadButtonDataOffset button) const { + const u32 mask = static_cast(button); + return (frame.virtual_buttons & mask) != 0 && (frame.prev_virtual_buttons & mask) == 0; +} + +bool OskVirtualPadInputView::RepeatPressed(Libraries::Pad::OrbisPadButtonDataOffset button, + bool& prev_down_state, double& next_repeat_time, + bool* out_repeat) const { + const VirtualButtonRepeatResult result = + ProcessRepeatButton(Down(button), Pressed(button), ImGui::GetTime(), repeat_delay, + repeat_rate, prev_down_state, next_repeat_time); + if (out_repeat) { + *out_repeat = result.repeat; + } + return result.pressed; +} + +static void ResetOskShortcutRepeatState(OskShortcutRepeatState& state) { + state.prev_square_down = false; + state.prev_l1_down = false; + state.prev_r1_down = false; + state.l2_shortcut_armed = true; + state.square_next_repeat_time = 0.0; + state.l1_next_repeat_time = 0.0; + state.r1_next_repeat_time = 0.0; + state.triangle_next_repeat_time = 0.0; +} + +static bool ConsumeTriggerShortcutPress(bool trigger_down, bool trigger_edge_pressed, + float trigger_analog_value, bool& shortcut_armed) { + // Require clear release before accepting another trigger shortcut press. + constexpr float kTriggerReleaseThreshold = 0.20f; + if (!trigger_down && trigger_analog_value <= kTriggerReleaseThreshold) { + shortcut_armed = true; + } + if (!shortcut_armed || !trigger_edge_pressed) { + return false; + } + shortcut_armed = false; + return true; +} + +OskShortcutActionResult EvaluateOskShortcutAction(bool allow_osk_shortcuts, bool menu_modal, + bool evaluate_action, + const OskPadInputFrame& panel_input, + const OskVirtualPadInputView& virtual_pad_input, + u32 prev_virtual_buttons, + ImeKbLayoutFamily layout_family, + OskShortcutRepeatState& repeat_state) { + OskShortcutActionResult result{}; + if (!(allow_osk_shortcuts && !menu_modal)) { + ResetOskShortcutRepeatState(repeat_state); + return result; + } + if (!evaluate_action) { + return result; + } + + constexpr u32 kMaskTriangle = + static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Triangle); + + const bool l1_down = virtual_pad_input.Down(Libraries::Pad::OrbisPadButtonDataOffset::L1); + const bool l1_edge_pressed = + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::L1); + const bool l1_repeat_pressed = virtual_pad_input.RepeatPressed( + Libraries::Pad::OrbisPadButtonDataOffset::L1, repeat_state.prev_l1_down, + repeat_state.l1_next_repeat_time); + + const bool square_down = + virtual_pad_input.Down(Libraries::Pad::OrbisPadButtonDataOffset::Square); + const bool square_edge_pressed = + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::Square); + const bool square_repeat_pressed = virtual_pad_input.RepeatPressed( + Libraries::Pad::OrbisPadButtonDataOffset::Square, repeat_state.prev_square_down, + repeat_state.square_next_repeat_time); + + const bool r1_repeat_pressed = virtual_pad_input.RepeatPressed( + Libraries::Pad::OrbisPadButtonDataOffset::R1, repeat_state.prev_r1_down, + repeat_state.r1_next_repeat_time); + + const bool clear_all_shortcut_pressed = + (l1_down && square_edge_pressed) || (square_down && l1_edge_pressed); + + const bool l2_down = virtual_pad_input.Down(Libraries::Pad::OrbisPadButtonDataOffset::L2); + const bool l2_edge_pressed = + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::L2); + const float l2_analog = std::max(panel_input.l2_analog, l2_down ? 1.0f : 0.0f); + const bool l2_pressed = ConsumeTriggerShortcutPress(l2_down, l2_edge_pressed, l2_analog, + repeat_state.l2_shortcut_armed); + + const bool tri_down = + virtual_pad_input.Down(Libraries::Pad::OrbisPadButtonDataOffset::Triangle); + const bool tri_edge_pressed = + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::Triangle); + bool prev_virtual_triangle_down = (prev_virtual_buttons & kMaskTriangle) != 0; + const bool tri_pressed = virtual_pad_input.RepeatPressed( + Libraries::Pad::OrbisPadButtonDataOffset::Triangle, prev_virtual_triangle_down, + repeat_state.triangle_next_repeat_time); + const bool tri_space_pressed = tri_pressed && !l2_down; + + const bool symbols_shortcut_pressed = (l2_down && tri_edge_pressed) || (tri_down && l2_pressed); + if (symbols_shortcut_pressed) { + result.action = ImeKbKeyAction::SymbolsMode; + } else if (tri_space_pressed) { + result.action = ImeKbKeyAction::Space; + } else if (layout_family != ImeKbLayoutFamily::Symbols && + virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::L3)) { + result.action = ImeKbKeyAction::SpecialsMode; + } else if (virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::R2)) { + result.action = ImeKbKeyAction::Done; + } else if (clear_all_shortcut_pressed) { + result.clear_all = true; + } else if (square_repeat_pressed) { + result.action = ImeKbKeyAction::Backspace; + } else if (l1_repeat_pressed) { + result.action = ImeKbKeyAction::ArrowLeft; + } else if (r1_repeat_pressed) { + result.action = ImeKbKeyAction::ArrowRight; + } else if (virtual_pad_input.Pressed(Libraries::Pad::OrbisPadButtonDataOffset::R3)) { + result.action = ImeKbKeyAction::Settings; + } else if (layout_family != ImeKbLayoutFamily::Symbols && l2_pressed) { + result.action = ImeKbKeyAction::Shift; + } + return result; +} + +namespace { + +bool IsVirtualPadButtonDown(const u32 buttons, const u32 button_mask) { + return (buttons & button_mask) != 0; +} + +bool IsVirtualPadButtonPressed(const u32 buttons, const u32 prev_buttons, const u32 button_mask) { + return IsVirtualPadButtonDown(buttons, button_mask) && + !IsVirtualPadButtonDown(prev_buttons, button_mask); +} + +VirtualButtonRepeatResult ProcessVirtualPadButtonRepeat(const u32 buttons, const u32 prev_buttons, + const u32 button_mask, const double now, + const double repeat_delay, + const double repeat_rate, + bool& prev_down_state, + double& next_repeat_time) { + return ProcessRepeatButton(IsVirtualPadButtonDown(buttons, button_mask), + IsVirtualPadButtonPressed(buttons, prev_buttons, button_mask), now, + repeat_delay, repeat_rate, prev_down_state, next_repeat_time); +} + +} // namespace + +OskPadInputFrame ComputeOskPadInputFrame(const VirtualPadSnapshot& virtual_pad, + bool allow_osk_shortcuts, bool first_render, + OskPadInputState& state) { + constexpr u32 kMaskLeft = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Left); + constexpr u32 kMaskRight = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Right); + constexpr u32 kMaskUp = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Up); + constexpr u32 kMaskDown = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Down); + constexpr u32 kMaskCross = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Cross); + constexpr u32 kMaskTriangle = + static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Triangle); + constexpr u32 kMaskSquare = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Square); + constexpr u32 kMaskCircle = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::Circle); + constexpr u32 kMaskL1 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L1); + constexpr u32 kMaskR1 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R1); + constexpr u32 kMaskL2 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L2); + constexpr u32 kMaskR2 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R2); + constexpr u32 kMaskL3 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::L3); + constexpr u32 kMaskR3 = static_cast(Libraries::Pad::OrbisPadButtonDataOffset::R3); + + OskPadInputFrame frame{}; + frame.virtual_buttons = virtual_pad.buttons; + frame.prev_virtual_buttons = first_render ? frame.virtual_buttons : state.prev_virtual_buttons; + frame.l2_analog = virtual_pad.l2_analog; + + const auto virtual_down = [&](const u32 mask) { + return IsVirtualPadButtonDown(frame.virtual_buttons, mask); + }; + const auto virtual_pressed = [&](const u32 mask) { + return IsVirtualPadButtonPressed(frame.virtual_buttons, frame.prev_virtual_buttons, mask); + }; + + const ImGuiIO& io = ImGui::GetIO(); + const double now = ImGui::GetTime(); + const double repeat_delay = static_cast(io.KeyRepeatDelay); + const double repeat_rate = static_cast(io.KeyRepeatRate); + + frame.cross_down = virtual_down(kMaskCross); + + bool prev_virtual_cross_hold_down = + IsVirtualPadButtonDown(frame.prev_virtual_buttons, kMaskCross); + const VirtualButtonRepeatResult virtual_cross_repeat = ProcessRepeatButton( + virtual_down(kMaskCross), virtual_pressed(kMaskCross), now, repeat_delay, repeat_rate, + prev_virtual_cross_hold_down, state.virtual_cross_next_repeat_time); + + const bool prev_cross_down = first_render ? frame.cross_down : state.prev_virtual_cross_down; + frame.panel_activate_pressed_raw = frame.cross_down && !prev_cross_down; + frame.panel_activate_repeat_raw = virtual_cross_repeat.repeat; + + const VirtualButtonRepeatResult virtual_nav_left = ProcessVirtualPadButtonRepeat( + frame.virtual_buttons, frame.prev_virtual_buttons, kMaskLeft, now, repeat_delay, + repeat_rate, state.prev_virtual_dpad_left_down, state.virtual_dpad_left_next_repeat_time); + const VirtualButtonRepeatResult virtual_nav_right = ProcessVirtualPadButtonRepeat( + frame.virtual_buttons, frame.prev_virtual_buttons, kMaskRight, now, repeat_delay, + repeat_rate, state.prev_virtual_dpad_right_down, state.virtual_dpad_right_next_repeat_time); + const VirtualButtonRepeatResult virtual_nav_up = ProcessVirtualPadButtonRepeat( + frame.virtual_buttons, frame.prev_virtual_buttons, kMaskUp, now, repeat_delay, repeat_rate, + state.prev_virtual_dpad_up_down, state.virtual_dpad_up_next_repeat_time); + const VirtualButtonRepeatResult virtual_nav_down = ProcessVirtualPadButtonRepeat( + frame.virtual_buttons, frame.prev_virtual_buttons, kMaskDown, now, repeat_delay, + repeat_rate, state.prev_virtual_dpad_down_down, state.virtual_dpad_down_next_repeat_time); + + frame.virtual_lstick_dirs.left = virtual_pad.left_stick.x <= -kNavAxisThreshold; + frame.virtual_lstick_dirs.right = virtual_pad.left_stick.x >= kNavAxisThreshold; + frame.virtual_lstick_dirs.up = virtual_pad.left_stick.y <= -kNavAxisThreshold; + frame.virtual_lstick_dirs.down = virtual_pad.left_stick.y >= kNavAxisThreshold; + const bool prev_virtual_lstick_left_down = + first_render ? frame.virtual_lstick_dirs.left : state.prev_virtual_lstick_left_down; + const bool prev_virtual_lstick_right_down = + first_render ? frame.virtual_lstick_dirs.right : state.prev_virtual_lstick_right_down; + const bool prev_virtual_lstick_up_down = + first_render ? frame.virtual_lstick_dirs.up : state.prev_virtual_lstick_up_down; + const bool prev_virtual_lstick_down_down = + first_render ? frame.virtual_lstick_dirs.down : state.prev_virtual_lstick_down_down; + + const bool virtual_lstick_left_edge = + frame.virtual_lstick_dirs.left && !prev_virtual_lstick_left_down; + const bool virtual_lstick_right_edge = + frame.virtual_lstick_dirs.right && !prev_virtual_lstick_right_down; + const bool virtual_lstick_up_edge = + frame.virtual_lstick_dirs.up && !prev_virtual_lstick_up_down; + const bool virtual_lstick_down_edge = + frame.virtual_lstick_dirs.down && !prev_virtual_lstick_down_down; + + const bool raw_virtual_control_input = + virtual_pressed(kMaskLeft) || virtual_pressed(kMaskRight) || virtual_pressed(kMaskUp) || + virtual_pressed(kMaskDown) || virtual_lstick_left_edge || virtual_lstick_right_edge || + virtual_lstick_up_edge || virtual_lstick_down_edge || virtual_pressed(kMaskCross) || + virtual_pressed(kMaskTriangle) || virtual_pressed(kMaskSquare) || + virtual_pressed(kMaskCircle) || virtual_pressed(kMaskL1) || virtual_pressed(kMaskR1) || + virtual_pressed(kMaskL2) || virtual_pressed(kMaskR2) || virtual_pressed(kMaskL3) || + virtual_pressed(kMaskR3); + frame.raw_osk_control_input = raw_virtual_control_input; + + float stick_strength = 0.0f; + const StickNavDirection stick_dir = ResolveStickNavDirection( + virtual_pad.left_stick.x, virtual_pad.left_stick.y, &stick_strength); + const bool stick_dir_edge_pressed = + IsStickDirectionEdgePressed(stick_dir, virtual_lstick_left_edge, virtual_lstick_right_edge, + virtual_lstick_up_edge, virtual_lstick_down_edge); + + StickNavPulseState stick_nav_state{state.left_stick_repeat_dir, + state.left_stick_next_repeat_time}; + const StickNavPulseResult stick_nav_pulse = + ProcessStickNavPulse(allow_osk_shortcuts, stick_dir, stick_strength, stick_dir_edge_pressed, + stick_nav_state, now); + state.left_stick_repeat_dir = stick_nav_state.repeat_dir; + state.left_stick_next_repeat_time = stick_nav_state.next_repeat_time; + + frame.virtual_nav_left = virtual_nav_left.pressed; + frame.virtual_nav_right = virtual_nav_right.pressed; + frame.virtual_nav_up = virtual_nav_up.pressed; + frame.virtual_nav_down = virtual_nav_down.pressed; + frame.virtual_nav_left_repeat = virtual_nav_left.repeat; + frame.virtual_nav_right_repeat = virtual_nav_right.repeat; + frame.virtual_nav_up_repeat = virtual_nav_up.repeat; + frame.virtual_nav_down_repeat = virtual_nav_down.repeat; + frame.stick_nav_left = stick_nav_pulse.left; + frame.stick_nav_right = stick_nav_pulse.right; + frame.stick_nav_up = stick_nav_pulse.up; + frame.stick_nav_down = stick_nav_pulse.down; + frame.stick_nav_left_repeat = stick_nav_pulse.left_repeat; + frame.stick_nav_right_repeat = stick_nav_pulse.right_repeat; + frame.stick_nav_up_repeat = stick_nav_pulse.up_repeat; + frame.stick_nav_down_repeat = stick_nav_pulse.down_repeat; + + frame.nav_left = allow_osk_shortcuts && (frame.virtual_nav_left || frame.stick_nav_left); + frame.nav_right = allow_osk_shortcuts && (frame.virtual_nav_right || frame.stick_nav_right); + frame.nav_up = allow_osk_shortcuts && (frame.virtual_nav_up || frame.stick_nav_up); + frame.nav_down = allow_osk_shortcuts && (frame.virtual_nav_down || frame.stick_nav_down); + frame.nav_left_repeat = + allow_osk_shortcuts && (frame.virtual_nav_left_repeat || frame.stick_nav_left_repeat); + frame.nav_right_repeat = + allow_osk_shortcuts && (frame.virtual_nav_right_repeat || frame.stick_nav_right_repeat); + frame.nav_up_repeat = + allow_osk_shortcuts && (frame.virtual_nav_up_repeat || frame.stick_nav_up_repeat); + frame.nav_down_repeat = + allow_osk_shortcuts && (frame.virtual_nav_down_repeat || frame.stick_nav_down_repeat); + + frame.virtual_control_input = + allow_osk_shortcuts && + (virtual_nav_left.pressed || virtual_nav_right.pressed || virtual_nav_up.pressed || + virtual_nav_down.pressed || stick_nav_pulse.left || stick_nav_pulse.right || + stick_nav_pulse.up || stick_nav_pulse.down || virtual_pressed(kMaskCross) || + virtual_pressed(kMaskTriangle) || virtual_pressed(kMaskSquare) || + virtual_pressed(kMaskCircle) || virtual_pressed(kMaskL1) || virtual_pressed(kMaskR1) || + virtual_pressed(kMaskL2) || virtual_pressed(kMaskR2) || virtual_pressed(kMaskL3) || + virtual_pressed(kMaskR3)); + + frame.osk_control_input = + frame.virtual_control_input || (allow_osk_shortcuts && (frame.panel_activate_pressed_raw || + frame.panel_activate_repeat_raw)); + return frame; +} + +void CommitOskPadInputFrame(const OskPadInputFrame& frame, OskPadInputState& state) { + state.prev_virtual_buttons = frame.virtual_buttons; + state.prev_virtual_cross_down = frame.cross_down; + state.prev_virtual_lstick_left_down = frame.virtual_lstick_dirs.left; + state.prev_virtual_lstick_right_down = frame.virtual_lstick_dirs.right; + state.prev_virtual_lstick_up_down = frame.virtual_lstick_dirs.up; + state.prev_virtual_lstick_down_down = frame.virtual_lstick_dirs.down; +} + +void DisarmMenuActivate(bool& menu_activate_armed) { + menu_activate_armed = false; +} + +void RearmMenuActivateOnRelease(bool activate_down, bool& menu_activate_armed) { + if (!menu_activate_armed && !activate_down) { + menu_activate_armed = true; + } +} + +bool ConsumeMenuActivatePress(bool panel_activate_pressed, bool opened_menu_this_frame, + bool& menu_activate_armed) { + if (!menu_activate_armed || opened_menu_this_frame || !panel_activate_pressed) { + return false; + } + menu_activate_armed = false; + return true; +} + +} // namespace Libraries::Ime diff --git a/src/core/libraries/ime/ime_ui_shared.h b/src/core/libraries/ime/ime_ui_shared.h new file mode 100644 index 000000000..a455c7f4b --- /dev/null +++ b/src/core/libraries/ime/ime_ui_shared.h @@ -0,0 +1,358 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/ime/ime_kb_layout.h" +#include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" + +namespace Libraries::Ime { + +struct VirtualPadSnapshot { + u32 buttons = 0; + ImVec2 left_stick{}; + ImVec2 panel_delta{}; + float l2_analog = 0.0f; +}; + +constexpr double kPanelEdgeWrapHoldDelaySec = 0.5; +constexpr double kRepeatIntentWindowSec = 0.45; +constexpr float kSelectorFadeOutDurationSec = 0.2f; +constexpr float kSelectorPressPulseDurationSec = 0.2f; +constexpr float kSelectorPressPulseExpandBorderFactor = 1.0f; + +ImVec4 BrightenColor(ImU32 color, float delta); +int Utf16CountFromUtf8Range(const char* text, const char* end = nullptr); +int Utf8ByteIndexFromUtf16Index(const char* text, int utf16_index); +bool RejectInputCharByUtf16Limit(const ImGuiInputTextCallbackData* data, int max_utf16); +bool ClampInputBufferToUtf16Limit(ImGuiInputTextCallbackData* data, int max_utf16); +void DrawInactiveCaretOverlay(const ImRect& frame_rect, const char* text, int caret_byte, + int selection_start_byte, int selection_end_byte, + bool multiline = false); + +struct SelectorFadeState { + ImVec2 current_min{}; + ImVec2 current_max{}; + float current_corner_radius = 0.0f; + bool current_visible = false; + + ImVec2 previous_min{}; + ImVec2 previous_max{}; + float previous_corner_radius = 0.0f; + bool previous_visible = false; + double previous_started_at = 0.0; + double press_pulse_started_at = -1.0; +}; + +void TriggerSelectorPressPulse(SelectorFadeState& state, double now); +float ComputePressPulseExpand(double pulse_started_at, double now, float pulse_duration_sec, + float max_expand_px); +void UpdateSelectorFadeState(SelectorFadeState& state, ImVec2 pos, ImVec2 size, float inset, + float corner_radius, bool selected, double now); +void DrawSelectorFadeState(const SelectorFadeState& state, ImDrawList* draw_list, + ImU32 overlay_color, ImU32 border_color, float border_thickness, + float fade_duration_sec, double now, float current_expand_px = 0.0f); + +ImeKbLayoutSelection ResolveInitialKbLayoutSelection(OrbisImeExtOption ext_option, + OrbisImePanelPriority panel_priority); +void InitializeDefaultOskSelectionAnchor(const ImeKbLayoutSelection& layout_selection, + OrbisImeExtOption ext_option, int& pending_row, + int& pending_col, int& last_row, int& last_col); +VirtualPadSnapshot ReadVirtualPadSnapshot(Libraries::UserService::OrbisUserServiceUserId user_id, + float delta_time, bool include_imgui_fallback = true); + +struct OskPadInputState { + u32& prev_virtual_buttons; + bool& prev_virtual_cross_down; + bool& prev_virtual_lstick_left_down; + bool& prev_virtual_lstick_right_down; + bool& prev_virtual_lstick_up_down; + bool& prev_virtual_lstick_down_down; + int& left_stick_repeat_dir; + double& left_stick_next_repeat_time; + double& virtual_cross_next_repeat_time; + bool& prev_virtual_dpad_left_down; + bool& prev_virtual_dpad_right_down; + bool& prev_virtual_dpad_up_down; + bool& prev_virtual_dpad_down_down; + double& virtual_dpad_left_next_repeat_time; + double& virtual_dpad_right_next_repeat_time; + double& virtual_dpad_up_next_repeat_time; + double& virtual_dpad_down_next_repeat_time; +}; + +struct OskPadInputFrame { + u32 virtual_buttons = 0; + u32 prev_virtual_buttons = 0; + struct { + bool left = false; + bool right = false; + bool up = false; + bool down = false; + } virtual_lstick_dirs{}; + bool cross_down = false; + float l2_analog = 0.0f; + bool panel_activate_pressed_raw = false; + bool panel_activate_repeat_raw = false; + bool virtual_nav_left = false; + bool virtual_nav_right = false; + bool virtual_nav_up = false; + bool virtual_nav_down = false; + bool virtual_nav_left_repeat = false; + bool virtual_nav_right_repeat = false; + bool virtual_nav_up_repeat = false; + bool virtual_nav_down_repeat = false; + bool stick_nav_left = false; + bool stick_nav_right = false; + bool stick_nav_up = false; + bool stick_nav_down = false; + bool stick_nav_left_repeat = false; + bool stick_nav_right_repeat = false; + bool stick_nav_up_repeat = false; + bool stick_nav_down_repeat = false; + bool nav_left = false; + bool nav_right = false; + bool nav_up = false; + bool nav_down = false; + bool nav_left_repeat = false; + bool nav_right_repeat = false; + bool nav_up_repeat = false; + bool nav_down_repeat = false; + bool raw_osk_control_input = false; + bool virtual_control_input = false; + bool osk_control_input = false; +}; + +struct OskVirtualPadInputView { + const OskPadInputFrame& frame; + double repeat_delay = 0.0; + double repeat_rate = 0.0; + + explicit OskVirtualPadInputView(const OskPadInputFrame& input_frame, const ImGuiIO& io); + bool Down(Libraries::Pad::OrbisPadButtonDataOffset button) const; + bool Pressed(Libraries::Pad::OrbisPadButtonDataOffset button) const; + bool RepeatPressed(Libraries::Pad::OrbisPadButtonDataOffset button, bool& prev_down_state, + double& next_repeat_time, bool* out_repeat = nullptr) const; +}; + +struct OskShortcutRepeatState { + bool& prev_square_down; + bool& prev_l1_down; + bool& prev_r1_down; + bool& l2_shortcut_armed; + double& square_next_repeat_time; + double& l1_next_repeat_time; + double& r1_next_repeat_time; + double& triangle_next_repeat_time; +}; + +struct OskShortcutActionResult { + ImeKbKeyAction action = ImeKbKeyAction::None; + bool clear_all = false; +}; + +OskShortcutActionResult EvaluateOskShortcutAction(bool allow_osk_shortcuts, bool menu_modal, + bool evaluate_action, + const OskPadInputFrame& panel_input, + const OskVirtualPadInputView& virtual_pad_input, + u32 prev_virtual_buttons, + ImeKbLayoutFamily layout_family, + OskShortcutRepeatState& repeat_state); + +void CycleKeyboardCaseState(ImeKbLayoutSelection& selection); +void ToggleKeyboardFamilyMode(ImeKbLayoutSelection& selection, ImeKbLayoutFamily& alpha_family, + ImeKbLayoutFamily target_family); +bool FocusKeyboardActionKeySelection(const ImeKbLayoutSelection& selection, ImeKbKeyAction action, + int& out_row, int& out_col); +void FlipKeyboardModePage(ImeKbLayoutSelection& selection, int direction); + +template +inline void ApplyOskPanelNavToKeyboardParams(KeyboardDrawParams& kb_params, + const bool allow_osk_shortcuts, + const OskPadInputFrame& panel_input) { + kb_params.external_nav_left = + allow_osk_shortcuts && (panel_input.virtual_nav_left || panel_input.stick_nav_left); + kb_params.external_nav_right = + allow_osk_shortcuts && (panel_input.virtual_nav_right || panel_input.stick_nav_right); + kb_params.external_nav_up = + allow_osk_shortcuts && (panel_input.virtual_nav_up || panel_input.stick_nav_up); + kb_params.external_nav_down = + allow_osk_shortcuts && (panel_input.virtual_nav_down || panel_input.stick_nav_down); + kb_params.external_nav_left_repeat = + allow_osk_shortcuts && + ((panel_input.virtual_nav_left && panel_input.virtual_nav_left_repeat) || + (panel_input.stick_nav_left && panel_input.stick_nav_left_repeat)); + kb_params.external_nav_right_repeat = + allow_osk_shortcuts && + ((panel_input.virtual_nav_right && panel_input.virtual_nav_right_repeat) || + (panel_input.stick_nav_right && panel_input.stick_nav_right_repeat)); + kb_params.external_nav_up_repeat = + allow_osk_shortcuts && ((panel_input.virtual_nav_up && panel_input.virtual_nav_up_repeat) || + (panel_input.stick_nav_up && panel_input.stick_nav_up_repeat)); + kb_params.external_nav_down_repeat = + allow_osk_shortcuts && + ((panel_input.virtual_nav_down && panel_input.virtual_nav_down_repeat) || + (panel_input.stick_nav_down && panel_input.stick_nav_down_repeat)); +} + +OskPadInputFrame ComputeOskPadInputFrame(const VirtualPadSnapshot& virtual_pad, + bool allow_osk_shortcuts, bool first_render, + OskPadInputState& state); +void CommitOskPadInputFrame(const OskPadInputFrame& frame, OskPadInputState& state); + +void DisarmMenuActivate(bool& menu_activate_armed); +void RearmMenuActivateOnRelease(bool activate_down, bool& menu_activate_armed); +bool ConsumeMenuActivatePress(bool panel_activate_pressed, bool opened_menu_this_frame, + bool& menu_activate_armed); + +template +inline void OpenOskMainEditMenu(EditMenuPopupT& popup, int& edit_menu_index, + bool& menu_activate_armed) { + popup = EditMenuPopupT::Main; + edit_menu_index = 0; + DisarmMenuActivate(menu_activate_armed); +} + +template +inline void OpenOskActionsEditMenu(EditMenuPopupT& popup, int& edit_menu_index, + bool& menu_activate_armed) { + popup = EditMenuPopupT::Actions; + edit_menu_index = 0; + DisarmMenuActivate(menu_activate_armed); +} + +template +inline bool CloseOskEditMenuOnCancel(EditMenuPopupT& popup, bool& cancel_pressed, + bool& menu_activate_armed) { + if (popup == EditMenuPopupT::None || !cancel_pressed) { + return false; + } + cancel_pressed = false; + popup = EditMenuPopupT::None; + menu_activate_armed = true; + return true; +} + +template +inline bool DrawAndHandleOskEditMenuPopup( + EditMenuPopupT& popup, int& edit_menu_index, const PanelMetricsT& metrics, ImDrawList* draw, + bool pointer_navigation_active, bool nav_up, bool nav_down, bool cross_down, + bool panel_activate_pressed, bool opened_menu_this_frame, bool& menu_activate_armed, + bool clipboard_ready, int id_base, const char* item_button_id, bool close_on_outside_click, + ApplyActionFn&& apply_action) { + if (popup == EditMenuPopupT::None || draw == nullptr) { + return false; + } + + constexpr std::array kMainMenuItems = {"Select", "Select All", "Paste"}; + constexpr std::array kActionMenuItems = {"Copy", "Paste"}; + const bool is_main_menu = (popup == EditMenuPopupT::Main); + const int item_count = is_main_menu ? static_cast(kMainMenuItems.size()) + : static_cast(kActionMenuItems.size()); + const auto item_label = [&](int index) -> const char* { + return is_main_menu ? kMainMenuItems[static_cast(index)] + : kActionMenuItems[static_cast(index)]; + }; + const auto item_enabled = [&](int index) { + if ((is_main_menu && index == 2) || (!is_main_menu && index == 1)) { + return clipboard_ready; + } + return true; + }; + + if (!pointer_navigation_active) { + if (nav_up) { + edit_menu_index = (edit_menu_index + item_count - 1) % item_count; + } else if (nav_down) { + edit_menu_index = (edit_menu_index + 1) % item_count; + } + } + edit_menu_index = std::clamp(edit_menu_index, 0, item_count - 1); + + const float menu_w = std::min(metrics.kb_size.x * 0.42f, 280.0f); + const float menu_inner_pad = std::max(6.0f, metrics.key_gap * 0.7f); + const float item_gap = std::max(3.0f, metrics.key_gap * 0.35f); + const float item_h = std::max(28.0f, metrics.key_h * 0.70f); + const float menu_h = menu_inner_pad * 2.0f + item_h * static_cast(item_count) + + item_gap * static_cast(item_count - 1); + const ImVec2 menu_pos{ + metrics.kb_pos.x + (metrics.kb_size.x - menu_w) * 0.5f, + metrics.kb_pos.y + (metrics.kb_size.y - menu_h) * 0.5f, + }; + const ImVec2 menu_max{menu_pos.x + menu_w, menu_pos.y + menu_h}; + draw->AddRectFilled(menu_pos, menu_max, IM_COL32(16, 16, 16, 245), metrics.corner_radius); + draw->AddRect(menu_pos, menu_max, IM_COL32(100, 100, 100, 255), metrics.corner_radius); + + RearmMenuActivateOnRelease(cross_down, menu_activate_armed); + const bool menu_activate = ConsumeMenuActivatePress( + panel_activate_pressed, opened_menu_this_frame, menu_activate_armed); + bool click_activate = false; + for (int i = 0; i < item_count; ++i) { + const float item_y = menu_pos.y + menu_inner_pad + i * (item_h + item_gap); + const ImVec2 item_pos{menu_pos.x + menu_inner_pad, item_y}; + const ImVec2 item_size{menu_w - menu_inner_pad * 2.0f, item_h}; + const bool selected = (i == edit_menu_index); + const bool enabled = item_enabled(i); + const ImU32 item_bg = !enabled ? IM_COL32(30, 30, 30, 255) + : selected ? IM_COL32(60, 96, 146, 255) + : IM_COL32(45, 45, 45, 255); + draw->AddRectFilled(item_pos, {item_pos.x + item_size.x, item_pos.y + item_size.y}, item_bg, + metrics.corner_radius * 0.6f); + draw->AddRect(item_pos, {item_pos.x + item_size.x, item_pos.y + item_size.y}, + IM_COL32(90, 90, 90, 255), metrics.corner_radius * 0.6f); + + ImGui::PushID(id_base + i); + ImGui::SetCursorScreenPos(item_pos); + ImGui::PushItemFlag(ImGuiItemFlags_NoNav, true); + ImGui::InvisibleButton(item_button_id, item_size); + ImGui::PopItemFlag(); + if (ImGui::IsItemHovered()) { + edit_menu_index = i; + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && enabled) { + edit_menu_index = i; + click_activate = true; + } + ImGui::PopID(); + + const char* label = item_label(i); + const ImVec2 text_size = ImGui::CalcTextSize(label); + const ImVec2 text_pos{ + item_pos.x + (item_size.x - text_size.x) * 0.5f, + item_pos.y + (item_size.y - text_size.y) * 0.5f, + }; + const ImU32 text_col = + enabled ? IM_COL32(232, 232, 232, 255) : IM_COL32(128, 128, 128, 255); + draw->AddText(text_pos, text_col, label); + } + + const bool outside_click_close = close_on_outside_click && pointer_navigation_active && + !opened_menu_this_frame && + ImGui::IsMouseClicked(ImGuiMouseButton_Left, false) && + !ImGui::IsMouseHoveringRect(menu_pos, menu_max, false); + if (outside_click_close) { + popup = EditMenuPopupT::None; + menu_activate_armed = true; + return true; + } + + if ((menu_activate || click_activate) && item_enabled(edit_menu_index)) { + const EditMenuPopupT previous_popup = popup; + apply_action(previous_popup, edit_menu_index); + if (popup != EditMenuPopupT::None && popup != previous_popup) { + edit_menu_index = 0; + } + if (popup == EditMenuPopupT::None) { + menu_activate_armed = true; + } + } + return true; +} + +} // namespace Libraries::Ime diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index ab5729f3b..017749543 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]; diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index 8f498234b..e9ab0b7b2 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -8,6 +8,7 @@ #include "core/libraries/libs.h" #include "core/libraries/pad/pad_errors.h" #include "core/user_settings.h" +#include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "pad.h" @@ -339,7 +340,21 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& contro return 1; } + const bool gamepad_input_intercepted = ImGui::Core::IsGamepadInputCaptured(); for (int i = 0; i < num; i++) { + if (gamepad_input_intercepted) { + pData[i] = {}; + pData[i].buttons = OrbisPadButtonDataOffset::Intercepted; + pData[i].leftStick = {128, 128}; + pData[i].rightStick = {128, 128}; + pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + pData[i].connected = connected; + pData[i].timestamp = states[i].time; + pData[i].connectedCount = connected_count; + pData[i].deviceUniqueDataLen = 0; + continue; + } + pData[i].buttons = states[i].buttonsState; pData[i].leftStick.x = states[i].axes[static_cast(Input::Axis::LeftX)]; pData[i].leftStick.y = states[i].axes[static_cast(Input::Axis::LeftY)]; diff --git a/src/imgui/renderer/font_stack.cpp b/src/imgui/renderer/font_stack.cpp index e5d88b7a9..2a2830043 100644 --- a/src/imgui/renderer/font_stack.cpp +++ b/src/imgui/renderer/font_stack.cpp @@ -39,10 +39,12 @@ constexpr ImWchar kArabicRanges[] = { }; constexpr ImWchar kSymbolsRanges[] = { + 0x2000, 0x206F, // General punctuation 0x20A0, 0x20CF, // Currency symbols 0x2100, 0x214F, // Letterlike symbols 0x2190, 0x21FF, // Arrows 0x2200, 0x22FF, // Math operators + 0x2300, 0x23FF, // Misc technical (includes keyboard symbol) 0x2460, 0x24FF, // Enclosed alphanumerics 0x25A0, 0x25FF, // Geometric shapes 0x2600, 0x26FF, // Misc symbols @@ -96,6 +98,7 @@ const ImWchar* GetPrimaryTextRanges(ImFontAtlas* atlas) { rb.AddRanges(atlas->GetGlyphRangesCyrillic()); rb.AddRanges(atlas->GetGlyphRangesVietnamese()); rb.AddRanges(kLatinExtendedRanges); + rb.AddRanges(kSymbolsRanges); rb.BuildRanges(&ranges); } return ranges.Data; diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index ba0118d33..8402ddc7a 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include #include @@ -32,11 +34,31 @@ static std::deque> change_layers{}; static std::mutex change_layers_mutex{}; static ImGuiID dock_id; +static std::atomic force_gamepad_input_capture_count{0}; namespace ImGui { namespace Core { +void AcquireGamepadInputCapture() { + force_gamepad_input_capture_count.fetch_add(1, std::memory_order_relaxed); +} + +void ReleaseGamepadInputCapture() { + std::uint32_t expected = force_gamepad_input_capture_count.load(std::memory_order_relaxed); + while (expected != 0) { + if (force_gamepad_input_capture_count.compare_exchange_weak( + expected, expected - 1, std::memory_order_relaxed, std::memory_order_relaxed)) { + return; + } + } + LOG_WARNING(ImGui, "ReleaseGamepadInputCapture called with no active capture"); +} + +bool IsGamepadInputCaptured() { + return force_gamepad_input_capture_count.load(std::memory_order_relaxed) > 0; +} + void Initialize(const ::Vulkan::Instance& instance, const Frontend::WindowSDL& window, const u32 image_count, vk::Format surface_format, const vk::AllocationCallbacks* allocator) { @@ -161,14 +183,28 @@ bool ProcessEvent(SDL_Event* event) { } case SDL_EVENT_TEXT_INPUT: case SDL_EVENT_KEY_DOWN: { + if (IsGamepadInputCaptured()) { + // Keep keyboard events flowing through the regular input-binding path while IME/OSK + // captures gamepad input, so keyboard equivalents of pad buttons still update the + // virtual controller state. + return false; + } const auto& io = GetIO(); return io.WantCaptureKeyboard && io.Ctx->NavWindow != nullptr && io.Ctx->NavWindow->ID != dock_id; } case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: - case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: { + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: { + if (IsGamepadInputCaptured()) { + // Let controller events continue to input bindings so OSK shortcuts and virtual + // controller state keep updating; game-side pad reads are blocked in libScePad. + return false; + } const auto& io = GetIO(); return io.NavActive && io.Ctx->NavWindow != nullptr && io.Ctx->NavWindow->ID != dock_id; } diff --git a/src/imgui/renderer/imgui_core.h b/src/imgui/renderer/imgui_core.h index 36ccff138..c80ca0e80 100644 --- a/src/imgui/renderer/imgui_core.h +++ b/src/imgui/renderer/imgui_core.h @@ -28,6 +28,10 @@ void Shutdown(const vk::Device& device); bool ProcessEvent(SDL_Event* event); +void AcquireGamepadInputCapture(); +void ReleaseGamepadInputCapture(); +bool IsGamepadInputCaptured(); + ImGuiID NewFrame(bool is_reusing_frame = false); void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view,