Add IME keyboard layout and panel metrics support (#3973)

* Add IME keyboard layout and panel metrics support

- Introduced new header and implementation files for IME keyboard layout handling.
- Added structures for viewport metrics, keyboard grid layout, and drawing parameters.
- Implemented functions to compute viewport and panel metrics for the IME dialog.
- Enhanced the IME dialog UI to utilize the new keyboard layout and metrics.
- Updated input handling to support new keyboard interactions and layout adjustments.
- Added caret management and text normalization features in the IME dialog state.
- Improved window positioning logic to accommodate different screen resolutions.

* fix for Maxos Linux builds

* Align IME behavior with real PS4 libSceIme/Dialog flow

Features:
- Add validation helpers for IME option/language/extended-option masks
- Implement `sceImeGetPanelSize` sizing logic
- Replace `sceImeSetTextGeometry` no-op path with real state/argument validation
- Align panel sizing/validation paths closer to PS4 IME behavior
- Improve panel text/caret handling reliability during UI updates
- Add IME panel movement via controller right stick
- Support mouse drag repositioning for the panel
- Respect `FIXED_POSITION` by disabling movement when locked

Updates:
- Tighten `sceImeOpen` checks (user/type/input method, handler, work alignment, reserved, overlap)
- Validate initial/runtime text rules (`\n`/`\r` and surrogate rejection) in open/set-text paths
- Enforce caret bounds and null-caret behavior in `sceImeSetCaret`
- Update `sceImeUpdate` to return `NOT_OPENED` in invalid states and respect handler matching
- Harden `ImeHandler` against null state/callback execution paths
- Optimize keyboard grid rendering by reusing per-frame buffers
- Reduce input callback allocations with fixed-size buffers
- Harden panel update paths for invalid handler/open-state cases
- Clamp panel position to visible screen bounds
- Apply stick deadzone/speed scaling for stable movement
- Keep panel coordinates consistent with IME coordinate mode

* 🤦‍♂️

* Enhance IME UI and Input Handling

- Introduced BrightenColor function to adjust color brightness for UI elements.
- Improved UTF-16 character handling with new functions to count UTF-16 units and reject input based on UTF-16 limits.
- Added functionality to clamp input buffer to UTF-16 limits, ensuring text input does not exceed specified limits.
- Refactored virtual pad input handling to encapsulate left stick directions and panel movement.
- Enhanced IME UI drawing logic to incorporate new input handling and navigation features.
- Updated keyboard layout handling to support dynamic configurations and improved navigation shortcuts.
- Integrated additional font ranges for better language support in the IME, including Chinese, Arabic, and Thai.
- Improved overall code structure and readability by utilizing modern C++ features such as std::vector and std::array.

* Remove non existing Roboto Medium font from IME initialization

* Enhance IME Dialog and UI with Improved Gamepad Navigation and Key Mapping

- Added support for disabling gamepad input through OrbisImeDisableDevice in ImeDialogState.
- Updated ImeKbLayout to modify key mappings for better character input, including changes to punctuation keys.
- Implemented enhanced left stick navigation with repeat functionality in ImeUi, allowing for smoother input handling.
- Introduced new constants for stick navigation delays and repeat intervals to improve responsiveness.
- Refactored input handling logic to accommodate both virtual and gamepad inputs, ensuring consistent behavior across devices.
- Added functionality to clear all text in the input field with a specific shortcut, aligning with expected user behavior.

* clang

* Enhance IME functionality with improved gamepad navigation and layout handling

* ime: fix specials/accent layout mapping and dynamic panel resize handling

- Expand specials/accent keyboard layouts to a 7-row model and move function keys to fixed bottom rows.
- Add variable row-height support to the keyboard grid (`fixed_bottom_rows`, `bottom_row_h`) and compute row offsets/span heights from layout config.
- Preserve function-row height in `ime_ui` and `ime_dialog_ui` while distributing remaining height across typing rows.
- Make mode-switch focus layout-driven by resolving `SymbolsMode`/`SpecialsMode` action keys in the active layout instead of relying on implicit row assumptions.
- Track panel layout anchor deltas in `ime_dialog_ui` and use press-offset dragging so cursor remains on the originally pressed point when panel dimensions/position change.

* Add IME UI enhancements and shared utilities

- Introduced new states for panel navigation and selector fade in `ime_ui.h`.
- Added a new header file `ime_ui_shared.h` containing utility functions and structures for virtual pad input handling, including deadzone application and stick navigation direction resolution.
- Updated `font_stack.cpp` to include additional Unicode ranges for general punctuation in the font atlas.

* Refactor IME settings: remove deprecated accessibility options and update references to use new settings structure

* Refactor IME UI Navigation and Activation Logic

- Introduced a new mechanism for handling virtual button repeat actions, improving responsiveness for gamepad navigation.
- Added support for stick navigation with adjustable repeat rates and initial delays, enhancing user experience during input.
- Removed the ImeSelectorFadeState structure and related logic to streamline the IME keyboard layout drawing process.
- Simplified the activation logic for menus, ensuring that menu activation is more intuitive and responsive to user input.
- Adjusted the navigation threshold for stick inputs to improve sensitivity and control.
- Cleaned up the code by removing unused variables and consolidating repeated logic into reusable functions.

* Refactor IME UI shared header: streamline code and improve structure

- Removed unused includes and redundant structures.
- Consolidated virtual pad snapshot handling and input state management.
- Introduced new structures for OSK pad input and navigation handling.
- Updated function signatures for clarity and consistency.
- Enhanced keyboard parameter application for OSK shortcuts.
- Improved overall readability and maintainability of the code.

* Refactor OskShortcutActionResult to simplify triangle button press handling

* Enhance IME Keyboard Layout and UI Functionality

- Added new key glyphs for Shift and Caps Lock in ime_kb_layout.h.
- Improved keyboard navigation logic to skip non-action keys.
- Introduced new constants for selector IDs in ime_ui.cpp for better readability.
- Refactored keyboard row and column clamping logic for clarity.
- Enhanced selector drawing logic with fade and pulse effects for better user feedback.
- Implemented functions for cycling keyboard case states and toggling keyboard family modes.
- Added functionality to focus on keyboard action keys based on their actions.
- Improved edit menu handling with new templated functions for better code reuse.
- Updated caret blinking logic to improve text input experience.
- Expanded symbol ranges in font_stack.cpp to include keyboard symbols.

---------

Co-authored-by: w1naenator <valdis.bogdans@hotmail.com>
This commit is contained in:
Valdis Bogdāns 2026-05-09 23:41:14 +03:00 committed by GitHub
parent c79abb6df4
commit 05df651cd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 9494 additions and 597 deletions

View File

@ -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

View File

@ -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();

View File

@ -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");
}

View File

@ -291,6 +291,8 @@ struct InputSettings {
Setting<bool> use_unified_input_config{true};
Setting<std::string> default_controller_id{""};
Setting<bool> background_controller_input{false}; // specific
Setting<bool> ime_accessibility_enabled{false}; // specific
Setting<bool> ime_url_mail_short_panel{false}; // specific
Setting<s32> camera_id{-1};
std::vector<OverrideItem> GetOverrideableFields() const {
@ -303,13 +305,18 @@ struct InputSettings {
&InputSettings::motion_controls_enabled),
make_override<InputSettings>("background_controller_input",
&InputSettings::background_controller_input),
make_override<InputSettings>("ime_accessibility_enabled",
&InputSettings::ime_accessibility_enabled),
make_override<InputSettings>("ime_url_mail_short_panel",
&InputSettings::ime_url_mail_short_panel),
make_override<InputSettings>("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)

View File

@ -1,11 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring>
#include <queue>
#include "common/logging/log.h"
#include "core/libraries/ime/ime.h"
#include "core/libraries/ime/ime_dialog.h"
#include "core/libraries/ime/ime_error.h"
#include "core/libraries/ime/ime_ui.h"
#include "core/libraries/kernel/process.h"
#include "core/libraries/libs.h"
#include "core/tls.h"
@ -15,6 +19,152 @@ static std::queue<OrbisImeEvent> g_ime_events;
static std::unique_ptr<ImeState> g_ime_state;
static std::unique_ptr<ImeUi> g_ime_ui;
namespace {
u32 GetCompiledSdkVersion() {
s32 ver = 0;
if (Libraries::Kernel::sceKernelGetCompiledSdkVersion(&ver) != ORBIS_OK) {
return 0;
}
return static_cast<u32>(ver);
}
u32 GetImeOptionMask() {
const u32 sdk = GetCompiledSdkVersion();
u32 mask = static_cast<u32>(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<u32>(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<u16>(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<uintptr_t>(param->inputTextBuffer);
const auto work_addr = reinterpret_cast<uintptr_t>(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<uintptr_t>(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<std::mutex> 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<u32>(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<u32>(param->option) & ~0x7BFF) { // Basic check for invalid options
LOG_ERROR(Lib_Ime, "Invalid option: {:032b}", static_cast<u32>(param->option));
return Error::INVALID_OPTION;
}
switch (param->type) {
case OrbisImeType::Default:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: Default ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::BasicLatin:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: BasicLatin ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Url:
*width = 500; // dummy value
*height = 100; // dummy value
LOG_DEBUG(Lib_Ime, "param->type: Url ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Mail:
// We set our custom sizes, commented sizes are the original ones
*width = 500; // 793
*height = 100; // 408
LOG_DEBUG(Lib_Ime, "param->type: Mail ({})", static_cast<u32>(param->type));
break;
case OrbisImeType::Number:
*width = 370;
*height = 402;
LOG_DEBUG(Lib_Ime, "param->type: Number ({})", static_cast<u32>(param->type));
break;
default:
LOG_ERROR(Lib_Ime, "Invalid param->type: ({})", static_cast<u32>(param->type));
if (static_cast<u32>(param->type) > static_cast<u32>(OrbisImeType::Number)) {
return Error::INVALID_TYPE;
}
LOG_DEBUG(Lib_Ime, "IME panel size: width={}, height={}", *width, *height);
const u32 option = static_cast<u32>(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<u32>(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<u32>(param->user_id));
return Error::INVALID_USER_ID;
}
if (!magic_enum::enum_contains(param->type)) {
if (static_cast<u32>(param->type) > static_cast<u32>(OrbisImeType::Number)) {
LOG_ERROR(Lib_Ime, "Invalid type: {}", static_cast<u32>(param->type));
return Error::INVALID_TYPE;
}
if (static_cast<u64>(param->supported_languages) & ~kValidOrbisImeLanguageMask) {
const u64 lang_mask = GetImeLanguageMask();
if ((~lang_mask & static_cast<u64>(param->supported_languages)) != 0) {
LOG_ERROR(Lib_Ime,
"Invalid supported_languages\n"
"supported_languages: {:064b}\n"
"valid_mask: {:064b}",
static_cast<u64>(param->supported_languages), kValidOrbisImeLanguageMask);
static_cast<u64>(param->supported_languages), lang_mask);
return Error::INVALID_SUPPORTED_LANGUAGES;
}
if (!magic_enum::enum_contains(param->enter_label)) {
if (static_cast<u32>(param->enter_label) > static_cast<u32>(OrbisImeEnterLabel::Go)) {
LOG_ERROR(Lib_Ime, "Invalid enter_label: {}", static_cast<u32>(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<u32>(param->input_method));
return Error::INVALID_INPUT_METHOD;
}
if (static_cast<u32>(param->option) & ~kValidImeOptionMask) {
LOG_ERROR(Lib_Ime, "option has invalid bits set (0x{:X}), mask=(0x{:X})",
static_cast<u32>(param->option), kValidImeOptionMask);
const u32 option = static_cast<u32>(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<u32>(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<u32>(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<u32>(param->horizontal_alignment) > 2U) {
LOG_ERROR(Lib_Ime, "Invalid horizontal_alignment: {}",
static_cast<u32>(param->horizontal_alignment));
return Error::INVALID_HORIZONTALIGNMENT;
}
if (!magic_enum::enum_contains(param->vertical_alignment)) {
if (static_cast<u32>(param->vertical_alignment) > 2U) {
LOG_ERROR(Lib_Ime, "Invalid vertical_alignment: {}",
static_cast<u32>(param->vertical_alignment));
return Error::INVALID_VERTICALALIGNMENT;
}
if (extended) {
u32 ext_option_value = static_cast<u32>(extended->option);
if (ext_option_value & ~kValidImeExtOptionMask) {
if (static_cast<u32>(extended->priority) >
static_cast<u32>(OrbisImePanelPriority::Accent)) {
LOG_ERROR(Lib_Ime, "Invalid extended->priority: {}",
static_cast<u32>(extended->priority));
return Error::INVALID_EXTENDED;
}
const u32 ext_option_value = static_cast<u32>(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<u32>(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<uintptr_t>(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<ImeHandler>(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<u32>(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<int>(Error::NOT_OPENED);
}
if (!geometry) {
return static_cast<int>(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<int>(Error::INVALID_PARAM);
}
if (mode != OrbisImeTextAreaMode::Select && mode != OrbisImeTextAreaMode::Preedit) {
return static_cast<int>(Error::INVALID_PARAM);
}
return static_cast<int>(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;

View File

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

View File

@ -121,7 +121,8 @@ enum class OrbisImeExtOption : u32 {
DECLARE_ENUM_FLAG_OPERATORS(OrbisImeExtOption);
constexpr u32 kValidImeExtOptionMask = static_cast<u32>(
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;

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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<char16_t> original_text;
std::vector<char> title;
std::vector<char> 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);
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,561 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <cstddef>
#include <imgui.h>
#include "core/libraries/ime/ime_common.h"
namespace Libraries::Ime {
struct ImeViewportMetrics {
ImVec2 size{};
ImVec2 offset{};
float scale_x = 1.0f;
float scale_y = 1.0f;
float ui_scale = 1.0f;
float base_w = 1920.0f;
float base_h = 1080.0f;
};
struct ImeKbGridLayout {
ImVec2 pos{};
ImVec2 size{};
float key_gap_x = 0.0f;
float key_gap_y = 0.0f;
// 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<int>(t * static_cast<float>(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<u8>(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<u8>(ImeSelectionGridIndex::DefaultTopPanelRow);
u8 row_span = static_cast<u8>(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<int>(layout.cols));
const int grid_rows = std::max(1, static_cast<int>(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<int>(key.row);
const int col_start = static_cast<int>(key.col);
const int row_span = std::max(1, static_cast<int>(key.row_span));
const int col_span = std::max(1, static_cast<int>(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<int>(layout.cols));
const int grid_rows = std::max(1, static_cast<int>(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<OrbisImeLanguage>(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

File diff suppressed because it is too large Load Diff

View File

@ -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<ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1> current_text;
int caret_index = 0;
int caret_byte_index = 0;
bool caret_dirty = false;
std::queue<OrbisImeEvent> 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
}; // namespace Libraries::Ime

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,358 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <imgui_internal.h>
#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 <typename KeyboardDrawParams>
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 <typename EditMenuPopupT>
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 <typename EditMenuPopupT>
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 <typename EditMenuPopupT>
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 <typename EditMenuPopupT, typename PanelMetricsT, typename ApplyActionFn>
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<const char*, 3> kMainMenuItems = {"Select", "Select All", "Paste"};
constexpr std::array<const char*, 2> kActionMenuItems = {"Copy", "Paste"};
const bool is_main_menu = (popup == EditMenuPopupT::Main);
const int item_count = is_main_menu ? static_cast<int>(kMainMenuItems.size())
: static_cast<int>(kActionMenuItems.size());
const auto item_label = [&](int index) -> const char* {
return is_main_menu ? kMainMenuItems[static_cast<std::size_t>(index)]
: kActionMenuItems[static_cast<std::size_t>(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<float>(item_count) +
item_gap * static_cast<float>(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

View File

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

View File

@ -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<int>(Input::Axis::LeftX)];
pData[i].leftStick.y = states[i].axes[static_cast<int>(Input::Axis::LeftY)];

View File

@ -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;

View File

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
#include <cstdint>
#include <SDL3/SDL_events.h>
#include <imgui.h>
@ -32,11 +34,31 @@ static std::deque<std::pair<bool, ImGui::Layer*>> change_layers{};
static std::mutex change_layers_mutex{};
static ImGuiID dock_id;
static std::atomic<std::uint32_t> 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;
}

View File

@ -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,