diff --git a/CMakeLists.txt b/CMakeLists.txt index adff454b8..91829261d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,6 +510,10 @@ set(IME_LIB src/core/libraries/ime/error_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_keyboard_layouts.cpp + src/core/libraries/ime/ime_keyboard_layouts.h + src/core/libraries/ime/ime_keyboard_ui.cpp + src/core/libraries/ime/ime_keyboard_ui.h src/core/libraries/ime/ime.cpp src/core/libraries/ime/ime.h src/core/libraries/ime/ime_error.h diff --git a/src/core/libraries/ime/ime_dialog.cpp b/src/core/libraries/ime/ime_dialog.cpp index bee185787..f8e8bc137 100644 --- a/src/core/libraries/ime/ime_dialog.cpp +++ b/src/core/libraries/ime/ime_dialog.cpp @@ -148,7 +148,91 @@ OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus() { return g_ime_dlg_status; } +#include + +#include + +static std::string ConvertUtf16ToUtf8(const char16_t* src) { + if (!src) { + return "(null)"; + } + + std::string result; + while (*src) { + char16_t c = *src++; + + if (c < 0x80) { + result += static_cast(c); + } else if (c < 0x800) { + result += static_cast(0xC0 | (c >> 6)); + result += static_cast(0x80 | (c & 0x3F)); + } else { + result += static_cast(0xE0 | (c >> 12)); + result += static_cast(0x80 | ((c >> 6) & 0x3F)); + result += static_cast(0x80 | (c & 0x3F)); + } + } + + return result; +} + +void DumpImeDialogParam(const Libraries::ImeDialog::OrbisImeDialogParam* param, + const Libraries::ImeDialog::OrbisImeParamExtended* ext_param) { + if (!param) { + LOG_INFO(Lib_ImeDialog, "OpenImeDialog called with null param."); + return; + } + + // UTF-16 to UTF-8 conversion using your safe method + std::string title_utf8 = "(null)"; + std::string placeholder_utf8 = "(null)"; + title_utf8 = ConvertUtf16ToUtf8(param->title); + placeholder_utf8 = ConvertUtf16ToUtf8(param->placeholder); + + LOG_INFO( + Lib_ImeDialog, + "OpenImeDialog:\n" + " user_id={}, type={}, option=0x{:X}, max_text_length={}, supported_languages=0x{:X}\n" + " title=\"{}\", placeholder=\"{}\", input_text_buffer={}", + param->user_id, static_cast(param->type), static_cast(param->option), + param->max_text_length, param->supported_languages, title_utf8, placeholder_utf8, + param->input_text_buffer ? reinterpret_cast(param->input_text_buffer) : 0); + + if (ext_param) { + LOG_INFO(Lib_ImeDialog, + "ExtendedParam:\n" + " color_base=({}, {}, {}, {}) color_line=({}, {}, {}, {}) color_text_field=({}, " + "{}, {}, {})\n" + " color_preedit=({}, {}, {}, {}) color_button_default=({}, {}, {}, {}) " + "color_button_function=({}, {}, {}, {})\n" + " color_button_symbol=({}, {}, {}, {}) color_text=({}, {}, {}, {}) " + "color_special=({}, {}, {}, {})\n" + " priority={}", + ext_param->color_base.r, ext_param->color_base.g, ext_param->color_base.b, + ext_param->color_base.a, ext_param->color_line.r, ext_param->color_line.g, + ext_param->color_line.b, ext_param->color_line.a, ext_param->color_text_field.r, + ext_param->color_text_field.g, ext_param->color_text_field.b, + ext_param->color_text_field.a, ext_param->color_preedit.r, + ext_param->color_preedit.g, ext_param->color_preedit.b, ext_param->color_preedit.a, + ext_param->color_button_default.r, ext_param->color_button_default.g, + ext_param->color_button_default.b, ext_param->color_button_default.a, + ext_param->color_button_function.r, ext_param->color_button_function.g, + ext_param->color_button_function.b, ext_param->color_button_function.a, + ext_param->color_button_symbol.r, ext_param->color_button_symbol.g, + ext_param->color_button_symbol.b, ext_param->color_button_symbol.a, + ext_param->color_text.r, ext_param->color_text.g, ext_param->color_text.b, + ext_param->color_text.a, ext_param->color_special.r, ext_param->color_special.g, + ext_param->color_special.b, ext_param->color_special.a, + static_cast(ext_param->priority)); + } else { + LOG_INFO(Lib_ImeDialog, "ExtendedParam: (none)"); + } +} + Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended) { + + DumpImeDialogParam(param, extended); + if (g_ime_dlg_status != OrbisImeDialogStatus::None) { LOG_INFO(Lib_ImeDialog, "IME dialog is already running"); return Error::BUSY; @@ -221,7 +305,7 @@ Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExt } if (param->max_text_length > ORBIS_IME_DIALOG_MAX_TEXT_LENGTH) { - LOG_INFO(Lib_ImeDialog, "Invalid param->maxTextLength"); + LOG_INFO(Lib_ImeDialog, "Invalid param->maxTextLength ({})", param->max_text_length); return Error::INVALID_MAX_TEXT_LENGTH; } diff --git a/src/core/libraries/ime/ime_dialog_ui.cpp b/src/core/libraries/ime/ime_dialog_ui.cpp index 51183c79b..92a1a630b 100644 --- a/src/core/libraries/ime/ime_dialog_ui.cpp +++ b/src/core/libraries/ime/ime_dialog_ui.cpp @@ -13,18 +13,48 @@ #include "core/tls.h" #include "imgui/imgui_std.h" +#include "core/libraries/ime/ime_keyboard_layouts.h" // c16rtomb, layout tables +#include "core/libraries/ime/ime_keyboard_ui.h" // DrawVirtualKeyboard, Utf8SafeBackspace + using namespace ImGui; +/* small helper for the OK/Cancel buttons */ static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f}; +/* convert palette colour from Orbis struct to ImGui format */ +static ImU32 ConvertColor(const Libraries::ImeDialog::OrbisImeColor& c) { + return IM_COL32(c.r, c.g, c.b, c.a); +} + +/*─────────────────────────────────────────────────────────────* + * Libraries::ImeDialog implementation + *─────────────────────────────────────────────────────────────*/ namespace Libraries::ImeDialog { +/* ---------------------------------------------------------- + * class‑static pointer – single definition + * ----------------------------------------------------------*/ +ImeDialogUi* ImeDialogUi::g_activeImeDialogUi = nullptr; + +/* ---------------------------------------------------------- + * keyboard‑to‑dialog event bridge + * ----------------------------------------------------------*/ +static void KeyboardCallbackBridge(const VirtualKeyEvent* evt) { + if (ImeDialogUi::g_activeImeDialogUi && evt) + ImeDialogUi::g_activeImeDialogUi->OnVirtualKeyEvent(evt); +} + +/*─────────────────────────────────────────────────────────────* + * ImeDialogState : constructors, helpers + *─────────────────────────────────────────────────────────────*/ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, - const OrbisImeParamExtended* extended) { + const OrbisImeParamExtended* extended) + : extended_param_(extended) { if (!param) { return; } + /* basic param copy */ user_id = param->user_id; is_multi_line = True(param->option & OrbisImeDialogOption::Multiline); is_numeric = param->type == OrbisImeType::Number; @@ -35,6 +65,7 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, max_text_length = param->max_text_length; text_buffer = param->input_text_buffer; + /* UTF‑16 → UTF‑8 conversions */ if (param->title) { std::size_t title_len = std::char_traits::length(param->title); title.resize(title_len * 4 + 1); @@ -166,12 +197,44 @@ bool ImeDialogState::ConvertUTF8ToOrbis(const char* utf8_text, std::size_t utf8_ return true; } +/*─────────────────────────────────────────────────────────────* + * ImeDialogUi : constructor / destructor / move + *─────────────────────────────────────────────────────────────*/ ImeDialogUi::ImeDialogUi(ImeDialogState* state, OrbisImeDialogStatus* status, OrbisImeDialogResult* result) : state(state), status(status), result(result) { + const OrbisImeParamExtended* incoming = state ? state->GetExtendedParam() : nullptr; + if (incoming) { + // copy caller’s palette + ext_ = *incoming; + } else { + // zero-init and then overwrite the color fields you need + std::memset(&ext_, 0, sizeof(ext_)); + ext_.color_base = {19, 19, 21, 240}; + ext_.color_line = {255, 255, 255, 255}; + ext_.color_text_field = {26, 26, 28, 240}; + ext_.color_preedit = {0, 0, 0, 255}; + ext_.color_button_default = {45, 45, 45, 255}; + ext_.color_button_function = {72, 72, 74, 255}; + ext_.color_button_symbol = {96, 96, 98, 255}; + ext_.color_text = {255, 255, 255, 255}; + ext_.color_special = {0, 123, 200, 255}; + ext_.priority = OrbisImePanelPriority::Default; + } + + // For text, lines, etc. + kb_style.color_text = ConvertColor(ext_.color_text); + kb_style.color_line = ConvertColor(ext_.color_line); + + // Button colors + kb_style.color_button_default = ConvertColor(ext_.color_button_default); + kb_style.color_button_symbol = ConvertColor(ext_.color_button_symbol); + kb_style.color_button_function = ConvertColor(ext_.color_button_function); + kb_style.color_special = ConvertColor(ext_.color_special); if (state && *status == OrbisImeDialogStatus::Running) { AddLayer(this); + ImeDialogUi::g_activeImeDialogUi = this; } } @@ -179,6 +242,10 @@ ImeDialogUi::~ImeDialogUi() { std::scoped_lock lock(draw_mutex); Free(); + + if (ImeDialogUi::g_activeImeDialogUi == this) { + ImeDialogUi::g_activeImeDialogUi = nullptr; + } } ImeDialogUi::ImeDialogUi(ImeDialogUi&& other) noexcept @@ -192,6 +259,7 @@ ImeDialogUi::ImeDialogUi(ImeDialogUi&& other) noexcept if (state && *status == OrbisImeDialogStatus::Running) { AddLayer(this); + ImeDialogUi::g_activeImeDialogUi = this; } } @@ -209,6 +277,7 @@ ImeDialogUi& ImeDialogUi::operator=(ImeDialogUi&& other) { if (state && *status == OrbisImeDialogStatus::Running) { AddLayer(this); + ImeDialogUi::g_activeImeDialogUi = this; } return *this; @@ -218,6 +287,9 @@ void ImeDialogUi::Free() { RemoveLayer(this); } +/*─────────────────────────────────────────────────────────────* + * ImeDialogUi : main ImGui draw routine + *─────────────────────────────────────────────────────────────*/ void ImeDialogUi::Draw() { std::unique_lock lock{draw_mutex}; @@ -228,16 +300,16 @@ void ImeDialogUi::Draw() { if (!status || *status != OrbisImeDialogStatus::Running) { return; } - + ImGui::PushStyleColor(ImGuiCol_WindowBg, ConvertColor(ext_.color_base)); const auto& ctx = *GetCurrentContext(); const auto& io = ctx.IO; ImVec2 window_size; if (state->is_multi_line) { - window_size = {500.0f, 300.0f}; + window_size = {500.0f, 500.0f}; } else { - window_size = {500.0f, 150.0f}; + window_size = {500.0f, 350.0f}; } CentralizeNextWindow(); @@ -250,79 +322,95 @@ void ImeDialogUi::Draw() { if (Begin("IME Dialog##ImeDialog", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { - DrawPrettyBackground(); + // DrawPrettyBackground(); - if (!state->title.empty()) { - SetWindowFontScale(1.7f); - TextUnformatted(state->title.data()); - SetWindowFontScale(1.0f); - } + /* ---------- title ---------- */ + DrawTitle(); + /* ---------- input box ---------- */ if (state->is_multi_line) { DrawMultiLineInputText(); } else { DrawInputText(); } - SetCursorPosY(GetCursorPosY() + 10.0f); + /* ---------- dummy prediction bar with Cancel button ---------- */ + DrawPredictionBarAnCancelButton(); - const char* button_text; + /* ---------- on‑screen keyboard ---------- */ + DrawVirtualKeyboardSection(); - switch (state->enter_label) { - case OrbisImeEnterLabel::Go: - button_text = "Go##ImeDialogOK"; - break; - case OrbisImeEnterLabel::Search: - button_text = "Search##ImeDialogOK"; - break; - case OrbisImeEnterLabel::Send: - button_text = "Send##ImeDialogOK"; - break; - case OrbisImeEnterLabel::Default: - default: - button_text = "OK##ImeDialogOK"; - break; - } + /* ---------- OK / Cancel buttons ---------- */ + /* + DrawOkAndCancelButtons(); + */ - 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; - } - - SameLine(0.0f, button_spacing); - - if (Button("Cancel##ImeDialogCancel", BUTTON_SIZE)) { - *status = OrbisImeDialogStatus::Finished; - result->endstatus = OrbisImeDialogEndStatus::UserCanceled; - } + End(); } - End(); + + ImGui::PopStyleColor(); first_render = false; } +/*─────────────────────────────────────────────────────────────* + * helper draw functions (unchanged) + *─────────────────────────────────────────────────────────────*/ void ImeDialogUi::DrawInputText() { + ImGui::BeginGroup(); + // ─── Apply ext_ colors ─────────────────────────────────────────────────── + ImGui::PushStyleColor(ImGuiCol_NavHighlight, ConvertColor(ext_.color_line)); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColor(ext_.color_text_field)); // background + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColor(ext_.color_text_field)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColor(ext_.color_text_field)); + ImGui::PushStyleColor(ImGuiCol_Border, ConvertColor(ext_.color_line)); // border line + ImGui::PushStyleColor(ImGuiCol_Text, ConvertColor(ext_.color_text)); // typed text + // ───────────────────────────────────────────────────────────────────────── ImVec2 input_size = {GetWindowWidth() - 40.0f, 0.0f}; SetCursorPosX(20.0f); if (first_render) { SetKeyboardFocusHere(); } + const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data(); if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(), state->max_text_length, input_size, ImGuiInputTextFlags_CallbackCharFilter, InputTextCallback, this)) { state->input_changed = true; } + + // CARET: manually render even if not focused + if (!ImGui::IsItemActive()) { + // Calculate input field position + ImVec2 input_pos = ImGui::GetItemRectMin(); + + // Find where to draw the caret + DrawCaretForInputText(state->current_text.begin(), state->caret_index, input_pos); + } + + // ────── replicate keyboard’s hover→nav focus highlight ────── + if (ImGui::IsItemHovered()) { + ImGui::SetItemCurrentNavFocus(); + ImGui::KeepNavHighlight(); + } + + // ────── pop ALL style colors (5 for input + 1 for NavHighlight) ────── + ImGui::PopStyleColor(6); + ImGui::EndGroup(); } void ImeDialogUi::DrawMultiLineInputText() { + ImGui::BeginGroup(); + // ─── Apply the same ext_ colors ─────────────────────────────────────────── + ImGui::PushStyleColor(ImGuiCol_NavHighlight, ConvertColor(ext_.color_line)); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColor(ext_.color_text_field)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColor(ext_.color_text_field)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColor(ext_.color_text_field)); + ImGui::PushStyleColor(ImGuiCol_Border, ConvertColor(ext_.color_line)); + ImGui::PushStyleColor(ImGuiCol_Text, ConvertColor(ext_.color_text)); + // ───────────────────────────────────────────────────────────────────────── ImVec2 input_size = {GetWindowWidth() - 40.0f, 200.0f}; SetCursorPosX(20.0f); ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackCharFilter | @@ -331,10 +419,18 @@ void ImeDialogUi::DrawMultiLineInputText() { SetKeyboardFocusHere(); } const char* placeholder = state->placeholder.empty() ? nullptr : state->placeholder.data(); + if (InputTextEx("##ImeDialogInput", placeholder, state->current_text.begin(), state->max_text_length, input_size, flags, InputTextCallback, this)) { state->input_changed = true; } + if (ImGui::IsItemHovered()) { + ImGui::SetItemCurrentNavFocus(); + ImGui::KeepNavHighlight(); + } + + ImGui::PopStyleColor(6); + ImGui::EndGroup(); } int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { @@ -359,8 +455,8 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { .keycode = 0, .character = 0, .status = 1, // ??? 1 = key pressed, 0 = key released - .type = OrbisImeKeyboardType::ENGLISH_US, // TODO set this to the correct value (maybe use - // the current language?) + .type = OrbisImeKeyboardType::ENGLISH_US, // TODO set this to the correct value (maybe + // use the current language?) .user_id = ui->state->user_id, .resource_id = 0, .timestamp = 0, @@ -382,4 +478,193 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { return 0; } +void ImeDialogUi::DrawTitle() { + if (!state->title.empty()) { + SetCursorPosX(20.0f); + ImGui::PushStyleColor(ImGuiCol_Text, ConvertColor(ext_.color_text)); + SetWindowFontScale(1.7f); + TextUnformatted(state->title.data()); + SetWindowFontScale(1.0f); + ImGui::PopStyleColor(); + } +} + +void ImeDialogUi::DrawOkAndCancelButtons() { + SetCursorPosY(GetCursorPosY() + 10.0f); + + const char* button_text; + + switch (state->enter_label) { + case OrbisImeEnterLabel::Go: + button_text = "Go##ImeDialogOK"; + break; + case OrbisImeEnterLabel::Search: + button_text = "Search##ImeDialogOK"; + break; + case OrbisImeEnterLabel::Send: + button_text = "Send##ImeDialogOK"; + break; + case OrbisImeEnterLabel::Default: + default: + button_text = "OK##ImeDialogOK"; + break; + } + + float button_spacing = 10.0f; + float total_button_width = BUTTON_SIZE.x * 2 + button_spacing; + float button_start_pos = (GetWindowWidth() - 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; + } + + SameLine(0.0f, button_spacing); + + if (Button("Cancel##ImeDialogCancel", BUTTON_SIZE)) { + *status = OrbisImeDialogStatus::Finished; + result->endstatus = OrbisImeDialogEndStatus::UserCanceled; + } +} + +/* draw keyboard in a sub‑ID scope */ +void ImeDialogUi::DrawVirtualKeyboardSection() { + ImGui::PushID("VirtualKeyboardSection"); + DrawVirtualKeyboard(kb_mode, state->type, shift_state, kb_language, KeyboardCallbackBridge, + kb_style); + ImGui::PopID(); +} + +void ImeDialogUi::DrawPredictionBarAnCancelButton() { + const float pad = 5.0f; + const float width = kb_style.layout_width; + const float bar_h = 25.0f; + SetCursorPosY(GetCursorPosY() + 5.0f); + SetCursorPosX(0.0f); + ImVec2 p0 = GetCursorScreenPos(); + ImVec2 p1 = ImVec2(p0.x + width - bar_h - 2 * pad, p0.y + bar_h); + // GetWindowDrawList()->AddRectFilled(p0, p1, IM_COL32(0, 0, 0, 255)); + + /* label */ + // ImGui::SetCursorScreenPos(ImVec2(p0.x, p0.y)); + // ImGui::PushStyleColor(ImGuiCol_Text, kb_style.color_text); + // Selectable("dummy prediction", false, 0, ImVec2(width - bar_h, bar_h)); + // ImGui::PopStyleColor(); + + SetCursorPosX(pad); + ImGui::PushStyleColor(ImGuiCol_NavHighlight, ConvertColor(ext_.color_line)); + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColor(ext_.color_preedit)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColor(ext_.color_preedit)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColor(ext_.color_preedit)); + ImGui::PushStyleColor(ImGuiCol_Text, ConvertColor(ext_.color_text)); + + if (ImGui::Button("predict", ImVec2(width - bar_h - 3 * pad, bar_h))) { + } + + if (ImGui::IsItemHovered()) { + ImGui::SetItemCurrentNavFocus(); + ImGui::KeepNavHighlight(); + } + + ImGui::PopStyleColor(4); + + /* X button */ + // ImGui::SameLine(width - bar_h); + ImGui::SetCursorScreenPos(ImVec2(p0.x + width - bar_h - pad, p0.y)); + + ImGui::PushStyleColor(ImGuiCol_Button, kb_style.color_button_function); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kb_style.color_button_function); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, kb_style.color_button_function); + ImGui::PushStyleColor(ImGuiCol_Text, kb_style.color_text); + + if (ImGui::Button("╳", ImVec2(bar_h, bar_h))) { + *status = OrbisImeDialogStatus::Finished; + result->endstatus = OrbisImeDialogEndStatus::UserCanceled; + } + + if (ImGui::IsItemHovered()) { + ImGui::SetItemCurrentNavFocus(); + ImGui::KeepNavHighlight(); + } + + ImGui::PopStyleColor(5); + + SetCursorPosX(0.0f); + SetCursorPosY(GetCursorPosY() + 5.0f); +} + +/*─────────────────────────────────────────────────────────────* + * helper draw functions (new) + *─────────────────────────────────────────────────────────────*/ +void ImeDialogUi::OnVirtualKeyEvent(const VirtualKeyEvent* evt) { + if (!evt || !state || !evt->key) + return; + + const KeyEntry* key = evt->key; + + /* Treat Repeat exactly like Down */ + if (evt->type == VirtualKeyEventType::Down || evt->type == VirtualKeyEventType::Repeat) { + switch (key->type) { + case KeyType::Character: { + char utf8[8]{}; + int n = c16rtomb(utf8, key->character); + if (n > 0) { + state->InsertUtf8AtCaret(utf8, (size_t)n); + } + break; + } + case KeyType::Function: + switch (key->keycode) { + case KC_LEFT: // Your custom code for ◀ button + if (state->caret_index > 0) + state->caret_index--; + LOG_INFO(Lib_ImeDialog, "Caret index = {}", state->caret_index); + break; + case KC_RIGHT: // Your custom code for ▶ button + if (state->caret_index < (int)state->current_text.size()) + state->caret_index++; + LOG_INFO(Lib_ImeDialog, "Caret index = {}", state->caret_index); + break; + case 0x08: + state->BackspaceUtf8AtCaret(); + break; // Backspace + case 0x0D: + *status = OrbisImeDialogStatus::Finished; // Enter + result->endstatus = OrbisImeDialogEndStatus::Ok; + break; + + case KC_SYM1: + kb_mode = KeyboardMode::Symbols1; + break; + case KC_SYM2: + kb_mode = KeyboardMode::Symbols2; + break; + case KC_ACCENTS: + kb_mode = KeyboardMode::AccentLetters; + break; + case KC_LETTERS: + kb_mode = KeyboardMode::Letters; + break; + + case 0x10: // Shift / Caps + case 0x105: + shift_state = (shift_state == ShiftState::None) + ? ShiftState::Shift + : (shift_state == ShiftState::Shift ? ShiftState::CapsLock + : ShiftState::None); + break; + default: + break; + } + break; + + default: + break; + } + } + /* Up is available if you need it later; currently ignored */ +} } // namespace Libraries::ImeDialog diff --git a/src/core/libraries/ime/ime_dialog_ui.h b/src/core/libraries/ime/ime_dialog_ui.h index 10dff5eeb..e1a3c0b40 100644 --- a/src/core/libraries/ime/ime_dialog_ui.h +++ b/src/core/libraries/ime/ime_dialog_ui.h @@ -3,21 +3,28 @@ #pragma once +#include // for strncpy / memcpy #include #include #include #include "common/cstring.h" #include "common/types.h" #include "core/libraries/ime/ime_dialog.h" +#include "core/libraries/ime/ime_keyboard_ui.h" #include "imgui/imgui_layer.h" namespace Libraries::ImeDialog { +// Forward declaration so we can befriend it class ImeDialogUi; +//--------------------------------------------------------------------- +// ImeDialogState — holds the text and options for the IME dialog +//--------------------------------------------------------------------- class ImeDialogState final { - friend ImeDialogUi; + friend class ImeDialogUi; // full access for the dialog‑UI layer + /*────────────────────────── private data ─────────────────────────*/ bool input_changed = false; s32 user_id{}; @@ -31,11 +38,17 @@ class ImeDialogState final { char16_t* text_buffer{}; std::vector title; std::vector placeholder; - + const OrbisImeParamExtended* extended_param_ = nullptr; // A character can hold up to 4 bytes in UTF-8 Common::CString current_text; + // Optional custom keyboard style (from extended params) + bool has_custom_style = false; + KeyboardStyle custom_kb_style{}; + int caret_index = 0; + public: + /*──────────────── constructors / rule‑of‑five ────────────────*/ ImeDialogState(const OrbisImeDialogParam* param = nullptr, const OrbisImeParamExtended* extended = nullptr); ImeDialogState(const ImeDialogState& other) = delete; @@ -44,6 +57,89 @@ public: bool CopyTextToOrbisBuffer(); bool CallTextFilter(); + /*──────────────────── public read helpers ───────────────────*/ + bool IsMultiLine() const { + return is_multi_line; + } + bool IsNumeric() const { + return is_numeric; + } + u32 MaxTextLength() const { + return max_text_length; + } + + const char* TitleUtf8() const { + return title.empty() ? nullptr : title.data(); + } + const char* PlaceholderUtf8() const { + return placeholder.empty() ? nullptr : placeholder.data(); + } + const char* CurrentTextUtf8() const { + return current_text.begin(); + } + + const OrbisImeParamExtended* GetExtendedParam() const { + return extended_param_; + } + + /*─────────────────── public write helpers ───────────────────*/ + // Replace the whole text buffer + void SetTextUtf8(const char* utf8) { + if (!utf8) + return; + std::strncpy(current_text.begin(), utf8, current_text.capacity() - 1); + current_text[current_text.capacity() - 1] = '\0'; + input_changed = true; + } + + void InsertUtf8AtCaret(const char* utf8, std::size_t len) { + if (!utf8 || len == 0) + return; + + std::size_t old_len = std::strlen(current_text.begin()); + if (old_len + len >= current_text.capacity()) + return; // full, silently ignore + + // Move the text after caret forward + char* text_begin = current_text.begin(); + std::memmove(text_begin + caret_index + len, text_begin + caret_index, + old_len - caret_index + 1); // +1 for null-terminator + + // Copy the inserted text at caret position + std::memcpy(text_begin + caret_index, utf8, len); + + caret_index += (int)len; // Move caret after inserted text + input_changed = true; + } + + // Remove one UTF‑8 code‑point from the end (safe backspace) + void BackspaceUtf8() { + Utf8SafeBackspace(current_text.begin()); + input_changed = true; + } + + void BackspaceUtf8AtCaret() { + char* buf = current_text.begin(); + size_t len = std::strlen(buf); + + if (caret_index == 0 || len == 0) + return; + + // Find byte index just before caret (start of previous codepoint) + int remove_start = caret_index - 1; + while (remove_start > 0 && + (static_cast(buf[remove_start]) & 0b11000000) == 0b10000000) + --remove_start; + + int remove_len = caret_index - remove_start; + + // Shift everything after caret to the left + std::memmove(buf + remove_start, buf + caret_index, + len - caret_index + 1); // +1 to move null terminator + caret_index = remove_start; + + input_changed = true; + } private: bool CallKeyboardFilter(const OrbisImeKeycode* src_keycode, u16* out_keycode, u32* out_status); @@ -54,7 +150,11 @@ private: char16_t* orbis_text, std::size_t orbis_text_len); }; +//--------------------------------------------------------------------- +// ImeDialogUi — draws the IME dialog & on‑screen keyboard +//--------------------------------------------------------------------- class ImeDialogUi final : public ImGui::Layer { + /*────────── private data ─────────*/ ImeDialogState* state{}; OrbisImeDialogStatus* status{}; OrbisImeDialogResult* result{}; @@ -62,7 +162,13 @@ class ImeDialogUi final : public ImGui::Layer { bool first_render = true; std::mutex draw_mutex; + OrbisImeParamExtended ext_; + public: + // Global pointer to the active dialog‑UI (used by the callback bridge) + static ImeDialogUi* g_activeImeDialogUi; + + /*───────── ctors / dtor ─────────*/ explicit ImeDialogUi(ImeDialogState* state = nullptr, OrbisImeDialogStatus* status = nullptr, OrbisImeDialogResult* result = nullptr); ~ImeDialogUi() override; @@ -70,15 +176,33 @@ public: ImeDialogUi(ImeDialogUi&& other) noexcept; ImeDialogUi& operator=(ImeDialogUi&& other); + /*────────── main draw ───────────*/ void Draw() override; + /*────────── keyboard events ─────*/ + void OnVirtualKeyEvent(const VirtualKeyEvent* evt); + private: + /*── helpers ─*/ void Free(); void DrawInputText(); void DrawMultiLineInputText(); static int InputTextCallback(ImGuiInputTextCallbackData* data); + + void DrawTitle(); + + void DrawOkAndCancelButtons(); + + /*── keyboard section ─*/ + KeyboardMode kb_mode = KeyboardMode::Letters; + ShiftState shift_state = ShiftState::None; + u64 kb_language = 0; + KeyboardStyle kb_style; + + void DrawVirtualKeyboardSection(); + void DrawPredictionBarAnCancelButton(); }; } // namespace Libraries::ImeDialog diff --git a/src/core/libraries/ime/ime_keyboard_layouts.cpp b/src/core/libraries/ime/ime_keyboard_layouts.cpp new file mode 100644 index 000000000..f7b10d4a6 --- /dev/null +++ b/src/core/libraries/ime/ime_keyboard_layouts.cpp @@ -0,0 +1,465 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/ime/ime_keyboard_layouts.h" + +int c16rtomb(char* out, char16_t ch) { + if (ch <= 0x7F) { + out[0] = static_cast(ch); + out[1] = '\0'; + return 1; + } else if (ch <= 0x7FF) { + out[0] = 0xC0 | ((ch >> 6) & 0x1F); + out[1] = 0x80 | (ch & 0x3F); + out[2] = '\0'; + return 2; + } else { + out[0] = 0xE0 | ((ch >> 12) & 0x0F); + out[1] = 0x80 | ((ch >> 6) & 0x3F); + out[2] = 0x80 | (ch & 0x3F); + out[3] = '\0'; + return 3; + } +} + +const std::vector kLayoutEnLettersUppercase = { + // Row 0 + {0x31, u'1', KeyType::Character, 0, 0, 1, 1, "1", "", {None, None}}, + {0x32, u'2', KeyType::Character, 0, 1, 1, 1, "2", "", {None, None}}, + {0x33, u'3', KeyType::Character, 0, 2, 1, 1, "3", "", {None, None}}, + {0x34, u'4', KeyType::Character, 0, 3, 1, 1, "4", "", {None, None}}, + {0x35, u'5', KeyType::Character, 0, 4, 1, 1, "5", "", {None, None}}, + {0x36, u'6', KeyType::Character, 0, 5, 1, 1, "6", "", {None, None}}, + {0x37, u'7', KeyType::Character, 0, 6, 1, 1, "7", "", {None, None}}, + {0x38, u'8', KeyType::Character, 0, 7, 1, 1, "8", "", {None, None}}, + {0x39, u'9', KeyType::Character, 0, 8, 1, 1, "9", "", {None, None}}, + {0x30, u'0', KeyType::Character, 0, 9, 1, 1, "0", "", {None, None}}, + + // Row 1 + {0x51, u'Q', KeyType::Character, 1, 0, 1, 1, "Q", "", {None, None}}, + {0x57, u'W', KeyType::Character, 1, 1, 1, 1, "W", "", {None, None}}, + {0x45, u'E', KeyType::Character, 1, 2, 1, 1, "E", "", {None, None}}, + {0x52, u'R', KeyType::Character, 1, 3, 1, 1, "R", "", {None, None}}, + {0x54, u'T', KeyType::Character, 1, 4, 1, 1, "T", "", {None, None}}, + {0x59, u'Y', KeyType::Character, 1, 5, 1, 1, "Y", "", {None, None}}, + {0x55, u'U', KeyType::Character, 1, 6, 1, 1, "U", "", {None, None}}, + {0x49, u'I', KeyType::Character, 1, 7, 1, 1, "I", "", {None, None}}, + {0x4F, u'O', KeyType::Character, 1, 8, 1, 1, "O", "", {None, None}}, + {0x50, u'P', KeyType::Character, 1, 9, 1, 1, "P", "", {None, None}}, + + // Row 2 + {0x41, u'A', KeyType::Character, 2, 0, 1, 1, "A", "", {None, None}}, + {0x53, u'S', KeyType::Character, 2, 1, 1, 1, "S", "", {None, None}}, + {0x44, u'D', KeyType::Character, 2, 2, 1, 1, "D", "", {None, None}}, + {0x46, u'F', KeyType::Character, 2, 3, 1, 1, "F", "", {None, None}}, + {0x47, u'G', KeyType::Character, 2, 4, 1, 1, "G", "", {None, None}}, + {0x48, u'H', KeyType::Character, 2, 5, 1, 1, "H", "", {None, None}}, + {0x4A, u'J', KeyType::Character, 2, 6, 1, 1, "J", "", {None, None}}, + {0x4B, u'K', KeyType::Character, 2, 7, 1, 1, "K", "", {None, None}}, + {0x4C, u'L', KeyType::Character, 2, 8, 1, 1, "L", "", {None, None}}, + {0x22, u'"', KeyType::Character, 2, 9, 1, 1, "\"", "", {None, None}}, + + // Row 3 + {0x5A, u'Z', KeyType::Character, 3, 0, 1, 1, "Z", "", {None, None}}, + {0x58, u'X', KeyType::Character, 3, 1, 1, 1, "X", "", {None, None}}, + {0x43, u'C', KeyType::Character, 3, 2, 1, 1, "C", "", {None, None}}, + {0x56, u'V', KeyType::Character, 3, 3, 1, 1, "V", "", {None, None}}, + {0x42, u'B', KeyType::Character, 3, 4, 1, 1, "B", "", {None, None}}, + {0x4E, u'N', KeyType::Character, 3, 5, 1, 1, "N", "", {None, None}}, + {0x4D, u'M', KeyType::Character, 3, 6, 1, 1, "M", "", {None, None}}, + {0x2D, u'-', KeyType::Character, 3, 7, 1, 1, "-", "", {None, None}}, + {0x5F, u'_', KeyType::Character, 3, 8, 1, 1, "_", "", {None, None}}, + {0x2F, u'/', KeyType::Character, 3, 9, 1, 1, "/", "", {None, None}}, + + // Row 4 + {0x10, u'\0', KeyType::Function, 4, 0, 1, 1, "⬆", "L2", {L2, None}}, + {KC_SYM1, u'\0', KeyType::Function, 4, 1, 1, 1, "@#:", "△", {Triangle, None}}, // TODO: + {KC_ACCENTS, u'\0', KeyType::Function, 4, 2, 1, 1, "à", "", {None, None}}, // TODO: + {0x20, u' ', KeyType::Character, 4, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x08, u'\0', KeyType::Function, 4, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 5 + {0xF020, u'\0', KeyType::Function, 5, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 5, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 5, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 5, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 5, 4, 1, 1, "KB", "", {None, None}}, // TODO: + {KC_OPT, u'\0', KeyType::Function, 5, 5, 1, 1, "...", "", {None, None}}, + {KC_GYRO, u'\0', KeyType::Function, 5, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO: + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x0D, u'\r', KeyType::Function, 5, 8, 2, 1, "Done", "R2", {R2, None}}, + +}; + +const std::vector kLayoutEnLettersLowercase = { + // Row 0 + {0x31, u'1', KeyType::Character, 0, 0, 1, 1, "1", "", {None, None}}, + {0x32, u'2', KeyType::Character, 0, 1, 1, 1, "2", "", {None, None}}, + {0x33, u'3', KeyType::Character, 0, 2, 1, 1, "3", "", {None, None}}, + {0x34, u'4', KeyType::Character, 0, 3, 1, 1, "4", "", {None, None}}, + {0x35, u'5', KeyType::Character, 0, 4, 1, 1, "5", "", {None, None}}, + {0x36, u'6', KeyType::Character, 0, 5, 1, 1, "6", "", {None, None}}, + {0x37, u'7', KeyType::Character, 0, 6, 1, 1, "7", "", {None, None}}, + {0x38, u'8', KeyType::Character, 0, 7, 1, 1, "8", "", {None, None}}, + {0x39, u'9', KeyType::Character, 0, 8, 1, 1, "9", "", {None, None}}, + {0x30, u'0', KeyType::Character, 0, 9, 1, 1, "0", "", {None, None}}, + + // Row 1 + {0x71, u'q', KeyType::Character, 1, 0, 1, 1, "q", "", {None, None}}, + {0x77, u'w', KeyType::Character, 1, 1, 1, 1, "w", "", {None, None}}, + {0x65, u'e', KeyType::Character, 1, 2, 1, 1, "e", "", {None, None}}, + {0x72, u'r', KeyType::Character, 1, 3, 1, 1, "r", "", {None, None}}, + {0x74, u't', KeyType::Character, 1, 4, 1, 1, "t", "", {None, None}}, + {0x79, u'y', KeyType::Character, 1, 5, 1, 1, "y", "", {None, None}}, + {0x75, u'u', KeyType::Character, 1, 6, 1, 1, "u", "", {None, None}}, + {0x69, u'i', KeyType::Character, 1, 7, 1, 1, "i", "", {None, None}}, + {0x6F, u'o', KeyType::Character, 1, 8, 1, 1, "o", "", {None, None}}, + {0x70, u'p', KeyType::Character, 1, 9, 1, 1, "p", "", {None, None}}, + + // Row 2 + {0x61, u'a', KeyType::Character, 2, 0, 1, 1, "a", "", {None, None}}, + {0x73, u's', KeyType::Character, 2, 1, 1, 1, "s", "", {None, None}}, + {0x64, u'd', KeyType::Character, 2, 2, 1, 1, "d", "", {None, None}}, + {0x66, u'f', KeyType::Character, 2, 3, 1, 1, "f", "", {None, None}}, + {0x67, u'g', KeyType::Character, 2, 4, 1, 1, "g", "", {None, None}}, + {0x68, u'h', KeyType::Character, 2, 5, 1, 1, "h", "", {None, None}}, + {0x6A, u'j', KeyType::Character, 2, 6, 1, 1, "j", "", {None, None}}, + {0x6B, u'k', KeyType::Character, 2, 7, 1, 1, "k", "", {None, None}}, + {0x6C, u'l', KeyType::Character, 2, 8, 1, 1, "l", "", {None, None}}, + {0x22, u'"', KeyType::Character, 2, 9, 1, 1, "\"", "", {None, None}}, + + // Row 3 + {0x7A, u'z', KeyType::Character, 3, 0, 1, 1, "z", "", {None, None}}, + {0x78, u'x', KeyType::Character, 3, 1, 1, 1, "x", "", {None, None}}, + {0x63, u'c', KeyType::Character, 3, 2, 1, 1, "c", "", {None, None}}, + {0x76, u'v', KeyType::Character, 3, 3, 1, 1, "v", "", {None, None}}, + {0x62, u'b', KeyType::Character, 3, 4, 1, 1, "b", "", {None, None}}, + {0x6E, u'n', KeyType::Character, 3, 5, 1, 1, "n", "", {None, None}}, + {0x6D, u'm', KeyType::Character, 3, 6, 1, 1, "m", "", {None, None}}, + {0x2D, u'-', KeyType::Character, 3, 7, 1, 1, "-", "", {None, None}}, + {0x5F, u'_', KeyType::Character, 3, 8, 1, 1, "_", "", {None, None}}, + {0x2F, u'/', KeyType::Character, 3, 9, 1, 1, "/", "", {None, None}}, + + // Row 4 + {0x105, u'\0', KeyType::Function, 4, 0, 1, 1, "⬆", "L2", {L2, None}}, + {KC_SYM1, u'\0', KeyType::Function, 4, 1, 1, 1, "@#:", "△", {Triangle, None}}, // TODO + {KC_ACCENTS, u'\0', KeyType::Function, 4, 2, 1, 1, "à", "", {None, None}}, // TODO + {0x20, u' ', KeyType::Character, 4, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x00, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x08, u'\0', KeyType::Function, 4, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 5 + {0xF020, u'\0', KeyType::Function, 5, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 5, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 5, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 5, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 5, 4, 1, 1, "KB", "", {None, None}}, // TODO + {KC_OPT, u'\0', KeyType::Function, 5, 5, 1, 1, "...", "", {None, None}}, // TODO + {KC_GYRO, u'\0', KeyType::Function, 5, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x0D, u'\r', KeyType::Function, 5, 8, 2, 1, "Done", "R2", {R2, None}}, + +}; + +// From PS5 +const std::vector kLayoutEnSymbols1 = { + // Row 1 + {0x21, u'!', KeyType::Character, 0, 0, 1, 1, "!", "", {None, None}}, + {0x3F, u'?', KeyType::Character, 0, 1, 1, 1, "?", "", {None, None}}, + {0x22, u'"', KeyType::Character, 0, 2, 1, 1, "\"", "", {None, None}}, + {0x27, u'\'', KeyType::Character, 0, 3, 1, 1, "'", "", {None, None}}, + {0x23, u'#', KeyType::Character, 0, 4, 1, 1, "#", "", {None, None}}, + {0x25, u'%', KeyType::Character, 0, 5, 1, 1, "%", "", {None, None}}, + {0x28, u'(', KeyType::Character, 0, 6, 1, 1, "(", "", {None, None}}, + {0x29, u')', KeyType::Character, 0, 7, 1, 1, ")", "", {None, None}}, + {0xF001, u'\0', KeyType::Function, 0, 8, 1, 1, "()", "", {None, None}}, + {0x2F, u'/', KeyType::Character, 0, 9, 1, 1, "/", "", {None, None}}, + + // Row 2 + {0x2D, u'-', KeyType::Character, 1, 0, 1, 1, "-", "", {None, None}}, + {0x5F, u'_', KeyType::Character, 1, 1, 1, 1, "_", "", {None, None}}, + {0x2C, u',', KeyType::Character, 1, 2, 1, 1, ",", "", {None, None}}, + {0x2E, u'.', KeyType::Character, 1, 3, 1, 1, ".", "", {None, None}}, + {0x3A, u':', KeyType::Character, 1, 4, 1, 1, ":", "", {None, None}}, + {0x3B, u';', KeyType::Character, 1, 5, 1, 1, ";", "", {None, None}}, + {0x2A, u'*', KeyType::Character, 1, 6, 1, 1, "*", "", {None, None}}, + {0x26, u'&', KeyType::Character, 1, 7, 1, 1, "&", "", {None, None}}, + {0x2B, u'+', KeyType::Character, 1, 8, 1, 1, "+", "", {None, None}}, + {0x3D, u'=', KeyType::Character, 1, 9, 1, 1, "=", "", {None, None}}, + + // Row 3 + {0x3C, u'<', KeyType::Character, 2, 0, 1, 1, "<", "", {None, None}}, + {0x3E, u'>', KeyType::Character, 2, 1, 1, 1, ">", "", {None, None}}, + {0x40, u'@', KeyType::Character, 2, 2, 1, 1, "@", "", {None, None}}, + {0x5B, u'[', KeyType::Character, 2, 3, 1, 1, "[", "", {None, None}}, + {0x5D, u']', KeyType::Character, 2, 4, 1, 1, "]", "", {None, None}}, + {0xF002, u'\0', KeyType::Function, 2, 5, 1, 1, "[]", "", {None, None}}, + {0x7B, u'{', KeyType::Character, 2, 6, 1, 1, "{", "", {None, None}}, + {0x7D, u'}', KeyType::Character, 2, 7, 1, 1, "}", "", {None, None}}, + {0xF004, u'\0', KeyType::Function, 2, 8, 1, 1, "{}", "", {None, None}}, + {KC_SYM2, u'\0', KeyType::Function, 2, 9, 1, 2, "→", "", {None, None}}, + + // Row 4 + {0x5C, u'\\', KeyType::Character, 3, 0, 1, 1, "\\", "", {None, None}}, + {0x7C, u'|', KeyType::Character, 3, 1, 1, 1, "|", "", {None, None}}, + {0x5E, u'^', KeyType::Character, 3, 2, 1, 1, "^", "", {None, None}}, + {0x60, u'`', KeyType::Character, 3, 3, 1, 1, "`", "", {None, None}}, + {0x24, u'$', KeyType::Character, 3, 4, 1, 1, "$", "", {None, None}}, + {0x20AC, u'\u20AC', KeyType::Character, 3, 5, 1, 1, "€", "", {None, None}}, + {0x00A3, u'\u00A3', KeyType::Character, 3, 6, 1, 1, "£", "", {None, None}}, + {0x00A5, u'\u00A5', KeyType::Character, 3, 7, 1, 1, "¥", "", {None, None}}, + {0x20A9, u'\u20A9', KeyType::Character, 3, 8, 1, 1, "₩", "", {None, None}}, + + // Row 5 + {0x0000, u'\0', KeyType::Disabled, 4, 0, 1, 1, "", "", {None, None}}, + {KC_LETTERS, u'\0', KeyType::Function, 4, 1, 1, 1, "ABC", "L2+△", {L2, Triangle}}, // TODO: + {0x0000, u'\0', KeyType::Disabled, 4, 2, 1, 1, "", "", {None, None}}, + {0x0020, u' ', KeyType::Character, 4, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x0008, u'\0', KeyType::Function, 4, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 6 + {0xF020, u'\0', KeyType::Function, 5, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 5, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 5, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 5, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 5, 4, 1, 1, "KB", "", {None, None}}, // TODO: + {KC_OPT, u'\0', KeyType::Function, 5, 5, 1, 1, "...", "", {None, None}}, // TODO: + {KC_GYRO, u'\0', KeyType::Function, 5, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO: + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x000D, u'\r', KeyType::Function, 5, 8, 2, 1, "Done", "R2", {R2, None}}, + +}; + +// From PS5 +const std::vector kLayoutEnSymbols2 = { + // Row 1 + {0x201C, u'“', KeyType::Character, 0, 0, 1, 1, "“", "", {None, None}}, + {0x201D, u'”', KeyType::Character, 0, 1, 1, 1, "”", "", {None, None}}, + {0x201E, u'„', KeyType::Character, 0, 2, 1, 1, "„", "", {None, None}}, + {0x00A1, u'¡', KeyType::Character, 0, 3, 1, 1, "¡", "", {None, None}}, + {0xF013, u'\0', KeyType::Function, 0, 4, 1, 1, "¡!", "", {None, None}}, + {0x00BF, u'¿', KeyType::Character, 0, 5, 1, 1, "¿", "", {None, None}}, + {0xF014, u'\0', KeyType::Function, 0, 6, 1, 1, "¿?", "", {None, None}}, + {0x007E, u'~', KeyType::Character, 0, 7, 1, 1, "~", "", {None, None}}, + {0x00B7, u'·', KeyType::Character, 0, 8, 1, 1, "·", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 0, 9, 1, 1, "", "", {None, None}}, + + // Row 2 + {0x00D7, u'×', KeyType::Character, 1, 0, 1, 1, "×", "", {None, None}}, + {0x00F7, u'÷', KeyType::Character, 1, 1, 1, 1, "÷", "", {None, None}}, + {0x2039, u'‹', KeyType::Character, 1, 2, 1, 1, "‹", "", {None, None}}, + {0x203A, u'›', KeyType::Character, 1, 3, 1, 1, "›", "", {None, None}}, + {0x00AB, u'«', KeyType::Character, 1, 4, 1, 1, "«", "", {None, None}}, + {0x00BB, u'»', KeyType::Character, 1, 5, 1, 1, "»", "", {None, None}}, + {0x00BA, u'º', KeyType::Character, 1, 6, 1, 1, "º", "", {None, None}}, + {0x00AA, u'ª', KeyType::Character, 1, 7, 1, 1, "ª", "", {None, None}}, + {0x00B0, u'°', KeyType::Character, 1, 8, 1, 1, "°", "", {None, None}}, + {0x00A7, u'§', KeyType::Character, 1, 9, 1, 1, "§", "", {None, None}}, + + // Row 3 + {KC_SYM1, u'\0', KeyType::Function, 2, 0, 1, 2, "←", "", {None, None}}, + {0x00A6, u'¦', KeyType::Character, 2, 1, 1, 1, "¦", "", {None, None}}, + {0x00B5, u'µ', KeyType::Character, 2, 2, 1, 1, "µ", "", {None, None}}, + {0x00AC, u'¬', KeyType::Character, 2, 3, 1, 1, "¬", "", {None, None}}, + {0x00B9, u'¹', KeyType::Character, 2, 4, 1, 1, "¹", "", {None, None}}, + {0x00B2, u'²', KeyType::Character, 2, 5, 1, 1, "²", "", {None, None}}, + {0x00B3, u'³', KeyType::Character, 2, 6, 1, 1, "³", "", {None, None}}, + {0x00BC, u'¼', KeyType::Character, 2, 7, 1, 1, "¼", "", {None, None}}, + {0x00BD, u'½', KeyType::Character, 2, 8, 1, 1, "½", "", {None, None}}, + {0x00BE, u'¾', KeyType::Character, 2, 9, 1, 1, "¾", "", {None, None}}, + + // Row 4 + {0x00A2, u'¢', KeyType::Character, 3, 1, 1, 1, "¢", "", {None, None}}, + {0x00A4, u'¤', KeyType::Character, 3, 2, 1, 1, "¤", "", {None, None}}, + {0x2019, u'’', KeyType::Character, 3, 3, 1, 1, "’", "", {None, None}}, + {0x2018, u'‘', KeyType::Character, 3, 4, 1, 1, "‘", "", {None, None}}, + {0x201B, u'‛', KeyType::Character, 3, 5, 1, 1, "‛", "", {None, None}}, + {0x201A, u'‚', KeyType::Character, 3, 6, 1, 1, "‚", "", {None, None}}, + {0x2116, u'№', KeyType::Character, 3, 7, 1, 1, "№", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 3, 8, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 3, 9, 1, 1, "", "", {None, None}}, + + // Row 5 + {0x0000, u'\0', KeyType::Disabled, 4, 0, 1, 1, "", "", {None, None}}, + {KC_LETTERS, u'\0', KeyType::Function, 4, 1, 1, 1, "ABC", "L2+△", {L2, Triangle}}, + {0x0000, u'\0', KeyType::Disabled, 4, 2, 1, 1, "", "", {None, None}}, + {0x20, u' ', KeyType::Character, 4, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x08, u'\0', KeyType::Function, 4, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 6 + {0xF020, u'\0', KeyType::Function, 5, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 5, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 5, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 5, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 5, 4, 1, 1, "KB", "", {None, None}}, // TODO + {KC_OPT, u'\0', KeyType::Function, 5, 5, 1, 1, "...", "", {None, None}}, // TODO + {KC_GYRO, u'\0', KeyType::Function, 5, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x0D, u'\r', KeyType::Function, 5, 8, 2, 1, "Done", "R2", {R2, None}}, + +}; + +const std::vector kLayoutEnAccentLettersUppercase = { + // Row 0 + {0x00C0, u'À', KeyType::Character, 0, 0, 1, 1, "À", "", {None, None}}, + {0x00C1, u'Á', KeyType::Character, 0, 1, 1, 1, "Á", "", {None, None}}, + {0x00C2, u'Â', KeyType::Character, 0, 2, 1, 1, "Â", "", {None, None}}, + {0x00C3, u'Ã', KeyType::Character, 0, 3, 1, 1, "Ã", "", {None, None}}, + {0x00C4, u'Ä', KeyType::Character, 0, 4, 1, 1, "Ä", "", {None, None}}, + {0x00C5, u'Å', KeyType::Character, 0, 5, 1, 1, "Å", "", {None, None}}, + {0x0104, u'Ą', KeyType::Character, 0, 6, 1, 1, "Ą", "", {None, None}}, + {0x00C6, u'Æ', KeyType::Character, 0, 7, 1, 1, "Æ", "", {None, None}}, + {0x00C7, u'Ç', KeyType::Character, 0, 8, 1, 1, "Ç", "", {None, None}}, + {0x0106, u'Ć', KeyType::Character, 0, 9, 1, 1, "Ć", "", {None, None}}, + + // Row 1 + {0x00C8, u'È', KeyType::Character, 1, 0, 1, 1, "È", "", {None, None}}, + {0x00C9, u'É', KeyType::Character, 1, 1, 1, 1, "É", "", {None, None}}, + {0x00CA, u'Ê', KeyType::Character, 1, 2, 1, 1, "Ê", "", {None, None}}, + {0x00CB, u'Ë', KeyType::Character, 1, 3, 1, 1, "Ë", "", {None, None}}, + {0x0118, u'Ę', KeyType::Character, 1, 4, 1, 1, "Ę", "", {None, None}}, + {0x011E, u'Ğ', KeyType::Character, 1, 5, 1, 1, "Ğ", "", {None, None}}, + {0x00CC, u'Ì', KeyType::Character, 1, 6, 1, 1, "Ì", "", {None, None}}, + {0x00CD, u'Í', KeyType::Character, 1, 7, 1, 1, "Í", "", {None, None}}, + {0x00CE, u'Î', KeyType::Character, 1, 8, 1, 1, "Î", "", {None, None}}, + {0x00CF, u'Ï', KeyType::Character, 1, 9, 1, 1, "Ï", "", {None, None}}, + + // Row 2 + {0x0130, u'İ', KeyType::Character, 2, 0, 1, 1, "İ", "", {None, None}}, + {0x0141, u'Ł', KeyType::Character, 2, 1, 1, 1, "Ł", "", {None, None}}, + {0x00D1, u'Ñ', KeyType::Character, 2, 2, 1, 1, "Ñ", "", {None, None}}, + {0x0143, u'Ń', KeyType::Character, 2, 3, 1, 1, "Ń", "", {None, None}}, + {0x00D2, u'Ò', KeyType::Character, 2, 4, 1, 1, "Ò", "", {None, None}}, + {0x00D3, u'Ó', KeyType::Character, 2, 5, 1, 1, "Ó", "", {None, None}}, + {0x00D4, u'Ô', KeyType::Character, 2, 6, 1, 1, "Ô", "", {None, None}}, + {0x00D5, u'Õ', KeyType::Character, 2, 7, 1, 1, "Õ", "", {None, None}}, + {0x00D6, u'Ö', KeyType::Character, 2, 8, 1, 1, "Ö", "", {None, None}}, + {0x00D8, u'Ø', KeyType::Character, 2, 9, 1, 1, "Ø", "", {None, None}}, + + // Row 3 + {0x0152, u'Œ', KeyType::Character, 3, 0, 1, 1, "Œ", "", {None, None}}, + {0x015A, u'Ś', KeyType::Character, 3, 1, 1, 1, "Ś", "", {None, None}}, + {0x015E, u'Ş', KeyType::Character, 3, 2, 1, 1, "Ş", "", {None, None}}, + {0x0160, u'Š', KeyType::Character, 3, 3, 1, 1, "Š", "", {None, None}}, + {0x00DF, u'ß', KeyType::Character, 3, 4, 1, 1, "ß", "", {None, None}}, + {0x00D9, u'Ù', KeyType::Character, 3, 5, 1, 1, "Ù", "", {None, None}}, + {0x00DA, u'Ú', KeyType::Character, 3, 6, 1, 1, "Ú", "", {None, None}}, + {0x00DB, u'Û', KeyType::Character, 3, 7, 1, 1, "Û", "", {None, None}}, + {0x00DC, u'Ü', KeyType::Character, 3, 8, 1, 1, "Ü", "", {None, None}}, + {0x00DD, u'Ý', KeyType::Character, 3, 9, 1, 1, "Ý", "", {None, None}}, + + // Row 4 + {0x0178, u'Ÿ', KeyType::Character, 4, 0, 1, 1, "Ÿ", "", {None, None}}, + {0x0179, u'Ź', KeyType::Character, 4, 1, 1, 1, "Ź", "", {None, None}}, + {0x017B, u'Ż', KeyType::Character, 4, 2, 1, 1, "Ż", "", {None, None}}, + {0x017D, u'Ž', KeyType::Character, 4, 3, 1, 1, "Ž", "", {None, None}}, + {0x00D0, u'Ð', KeyType::Character, 4, 4, 1, 1, "Ð", "", {None, None}}, + {0x00DE, u'Þ', KeyType::Character, 4, 5, 1, 1, "Þ", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 6, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 8, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 9, 1, 1, "", "", {None, None}}, + + // Row 5 + {0x0010, u'\0', KeyType::Function, 5, 0, 1, 1, "⬆", "L2", {L2, None}}, + {KC_LETTERS, u'\0', KeyType::Function, 5, 1, 1, 1, "ABC", "L2+△", {L3, Triangle}}, // TODO: + {0x0000, u'\0', KeyType::Disabled, 5, 2, 1, 1, "", "", {None, None}}, // TODO: + {0x0020, u' ', KeyType::Character, 5, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x0008, u'\0', KeyType::Function, 5, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 6 + {0xF020, u'\0', KeyType::Function, 6, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 6, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 6, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 6, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 6, 4, 1, 1, "KB", "", {None, None}}, // TODO + {KC_OPT, u'\0', KeyType::Function, 6, 5, 1, 1, "...", "", {None, None}}, // TODO + {KC_GYRO, u'\0', KeyType::Function, 6, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO + {0x0000, u'\0', KeyType::Disabled, 6, 7, 1, 1, "", "", {None, None}}, + {0x000D, u'\r', KeyType::Function, 6, 8, 2, 1, "Done", "R2", {R2, None}}, +}; + +const std::vector kLayoutEnAccentLettersLowercase = { + // Row 0 + {0x00E0, u'à', KeyType::Character, 0, 0, 1, 1, "à", "", {None, None}}, + {0x00E1, u'á', KeyType::Character, 0, 1, 1, 1, "á", "", {None, None}}, + {0x00E2, u'â', KeyType::Character, 0, 2, 1, 1, "â", "", {None, None}}, + {0x00E3, u'ã', KeyType::Character, 0, 3, 1, 1, "ã", "", {None, None}}, + {0x00E4, u'ä', KeyType::Character, 0, 4, 1, 1, "ä", "", {None, None}}, + {0x00E5, u'å', KeyType::Character, 0, 5, 1, 1, "å", "", {None, None}}, + {0x0105, u'ą', KeyType::Character, 0, 6, 1, 1, "ą", "", {None, None}}, + {0x00E6, u'æ', KeyType::Character, 0, 7, 1, 1, "æ", "", {None, None}}, + {0x00E7, u'ç', KeyType::Character, 0, 8, 1, 1, "ç", "", {None, None}}, + {0x0107, u'ć', KeyType::Character, 0, 9, 1, 1, "ć", "", {None, None}}, + + // Row 1 + {0x00E8, u'è', KeyType::Character, 1, 0, 1, 1, "è", "", {None, None}}, + {0x00E9, u'é', KeyType::Character, 1, 1, 1, 1, "é", "", {None, None}}, + {0x00EA, u'ê', KeyType::Character, 1, 2, 1, 1, "ê", "", {None, None}}, + {0x00EB, u'ë', KeyType::Character, 1, 3, 1, 1, "ë", "", {None, None}}, + {0x0119, u'ę', KeyType::Character, 1, 4, 1, 1, "ę", "", {None, None}}, + {0x011F, u'ğ', KeyType::Character, 1, 5, 1, 1, "ğ", "", {None, None}}, + {0x00EC, u'ì', KeyType::Character, 1, 6, 1, 1, "ì", "", {None, None}}, + {0x00ED, u'í', KeyType::Character, 1, 7, 1, 1, "í", "", {None, None}}, + {0x00EE, u'î', KeyType::Character, 1, 8, 1, 1, "î", "", {None, None}}, + {0x00EF, u'ï', KeyType::Character, 1, 9, 1, 1, "ï", "", {None, None}}, + + // Row 2 + {0x0131, u'ı', KeyType::Character, 2, 0, 1, 1, "ı", "", {None, None}}, + {0x0142, u'ł', KeyType::Character, 2, 1, 1, 1, "ł", "", {None, None}}, + {0x00F1, u'ñ', KeyType::Character, 2, 2, 1, 1, "ñ", "", {None, None}}, + {0x0144, u'ń', KeyType::Character, 2, 3, 1, 1, "ń", "", {None, None}}, + {0x00F2, u'ò', KeyType::Character, 2, 4, 1, 1, "ò", "", {None, None}}, + {0x00F3, u'ó', KeyType::Character, 2, 5, 1, 1, "ó", "", {None, None}}, + {0x00F4, u'ô', KeyType::Character, 2, 6, 1, 1, "ô", "", {None, None}}, + {0x00F5, u'õ', KeyType::Character, 2, 7, 1, 1, "õ", "", {None, None}}, + {0x00F6, u'ö', KeyType::Character, 2, 8, 1, 1, "ö", "", {None, None}}, + {0x00F8, u'ø', KeyType::Character, 2, 9, 1, 1, "ø", "", {None, None}}, + + // Row 3 + {0x0153, u'œ', KeyType::Character, 3, 0, 1, 1, "œ", "", {None, None}}, + {0x015B, u'ś', KeyType::Character, 3, 1, 1, 1, "ś", "", {None, None}}, + {0x015F, u'ş', KeyType::Character, 3, 2, 1, 1, "ş", "", {None, None}}, + {0x0161, u'š', KeyType::Character, 3, 3, 1, 1, "š", "", {None, None}}, + {0x00DF, u'ß', KeyType::Character, 3, 4, 1, 1, "ß", "", {None, None}}, + {0x00F9, u'ù', KeyType::Character, 3, 5, 1, 1, "ù", "", {None, None}}, + {0x00FA, u'ú', KeyType::Character, 3, 6, 1, 1, "ú", "", {None, None}}, + {0x00FB, u'û', KeyType::Character, 3, 7, 1, 1, "û", "", {None, None}}, + {0x00FC, u'ü', KeyType::Character, 3, 8, 1, 1, "ü", "", {None, None}}, + {0x00FD, u'ý', KeyType::Character, 3, 9, 1, 1, "ý", "", {None, None}}, + + // Row 4 + {0x00FF, u'ÿ', KeyType::Character, 4, 0, 1, 1, "ÿ", "", {None, None}}, + {0x017A, u'ź', KeyType::Character, 4, 1, 1, 1, "ź", "", {None, None}}, + {0x017C, u'ż', KeyType::Character, 4, 2, 1, 1, "ż", "", {None, None}}, + {0x017E, u'ž', KeyType::Character, 4, 3, 1, 1, "ž", "", {None, None}}, + {0x00F0, u'ð', KeyType::Character, 4, 4, 1, 1, "ð", "", {None, None}}, + {0x00FE, u'þ', KeyType::Character, 4, 5, 1, 1, "þ", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 6, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 7, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 8, 1, 1, "", "", {None, None}}, + {0x0000, u'\0', KeyType::Disabled, 4, 9, 1, 1, "", "", {None, None}}, + + // Row 5 + {0x0010, u'\0', KeyType::Function, 5, 0, 1, 1, "⬆", "L2", {L2, None}}, + {KC_LETTERS, u'\0', KeyType::Function, 5, 1, 1, 1, "ABC", "L2+△", {L3, Triangle}}, // TODO + {0x0000, u'\0', KeyType::Disabled, 5, 2, 1, 1, "", "", {None, None}}, // TODO + {0x0020, u' ', KeyType::Character, 5, 3, 4, 1, "Space", "△", {Triangle, None}}, + {0x0000, u'\0', KeyType::Disabled, 5, 7, 1, 1, "", "", {None, None}}, + {0x0008, u'\0', KeyType::Function, 5, 8, 2, 1, "⇦", "□", {Square, None}, true}, + + // Row 6 + {0xF020, u'\0', KeyType::Function, 6, 0, 1, 1, "▲", "", {Up, None}}, + {0xF021, u'\0', KeyType::Function, 6, 1, 1, 1, "▼", "", {Down, None}}, + {0xF022, u'\0', KeyType::Function, 6, 2, 1, 1, "◀", "L1", {L1, None}}, + {0xF023, u'\0', KeyType::Function, 6, 3, 1, 1, "▶", "R1", {R1, None}}, + {KC_KB, u'\0', KeyType::Function, 6, 4, 1, 1, "KB", "", {None, None}}, // TODO + {KC_OPT, u'\0', KeyType::Function, 6, 5, 1, 1, "...", "", {None, None}}, // TODO + {KC_GYRO, u'\0', KeyType::Function, 6, 6, 1, 1, "+/⊗", "R3", {R3, None}}, // TODO + {0x0000, u'\0', KeyType::Disabled, 6, 7, 1, 1, "", "", {None, None}}, + {0x000D, u'\r', KeyType::Function, 6, 8, 2, 1, "Done", "R2", {R2, None}}, +}; diff --git a/src/core/libraries/ime/ime_keyboard_layouts.h b/src/core/libraries/ime/ime_keyboard_layouts.h new file mode 100644 index 000000000..4e5cdc18a --- /dev/null +++ b/src/core/libraries/ime/ime_keyboard_layouts.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include "common/types.h" + +enum class KeyType : u8 { Character = 0, Function = 1, Disabled = 2 }; + +struct KeyEntry { + u16 keycode; // 0xF100+ unused, so can be used as temporary defined keys for unknown + char16_t character; + KeyType type; + u8 row; + u8 col; + u8 colspan; + u8 rowspan; + const char* label; + const char* controller_hint; + ImGuiKey bound_buttons[2]; + bool allow_repeat{false}; +}; + +int c16rtomb(char* out, char16_t ch); + +extern const std::vector kLayoutEnLettersUppercase; +extern const std::vector kLayoutEnLettersLowercase; +extern const std::vector kLayoutEnAccentLettersUppercase; +extern const std::vector kLayoutEnAccentLettersLowercase; +extern const std::vector kLayoutEnSymbols1; +extern const std::vector kLayoutEnSymbols2; + +constexpr ImGuiKey None = ImGuiKey::ImGuiKey_None; +constexpr ImGuiKey L1 = ImGuiKey::ImGuiKey_GamepadL1; +constexpr ImGuiKey R1 = ImGuiKey::ImGuiKey_GamepadR1; +constexpr ImGuiKey L2 = ImGuiKey::ImGuiKey_GamepadL2; +constexpr ImGuiKey R2 = ImGuiKey::ImGuiKey_GamepadR2; +constexpr ImGuiKey L3 = ImGuiKey::ImGuiKey_GamepadL3; +constexpr ImGuiKey R3 = ImGuiKey::ImGuiKey_GamepadR3; +constexpr ImGuiKey Up = ImGuiKey::ImGuiKey_GamepadDpadUp; +constexpr ImGuiKey Down = ImGuiKey::ImGuiKey_GamepadDpadDown; +constexpr ImGuiKey Left = ImGuiKey::ImGuiKey_GamepadDpadLeft; +constexpr ImGuiKey Right = ImGuiKey::ImGuiKey_GamepadDpadRight; +constexpr ImGuiKey Cross = ImGuiKey::ImGuiKey_GamepadFaceDown; // X button +constexpr ImGuiKey Circle = ImGuiKey::ImGuiKey_GamepadFaceRight; // O button +constexpr ImGuiKey Square = ImGuiKey::ImGuiKey_GamepadFaceLeft; // [] button +constexpr ImGuiKey Triangle = ImGuiKey::ImGuiKey_GamepadFaceUp; // /\ button +constexpr ImGuiKey Options = ImGuiKey::ImGuiKey_GraveAccent; // Options button + +// Fake function keycodes +constexpr u16 KC_SYM1 = 0xF100; +constexpr u16 KC_SYM2 = 0xF101; +constexpr u16 KC_ACCENTS = 0xF102; +constexpr u16 KC_LETTERS = 0xF103; +constexpr u16 KC_KB = 0xF104; +constexpr u16 KC_GYRO = 0xF105; +constexpr u16 KC_OPT = 0xF106; +constexpr u16 KC_UP = 0xF020; +constexpr u16 KC_DOWN = 0xF021; +constexpr u16 KC_LEFT = 0xF022; +constexpr u16 KC_RIGHT = 0xF023; diff --git a/src/core/libraries/ime/ime_keyboard_ui.cpp b/src/core/libraries/ime/ime_keyboard_ui.cpp new file mode 100644 index 000000000..1f5b6b97c --- /dev/null +++ b/src/core/libraries/ime/ime_keyboard_ui.cpp @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// #include +#include +#include "common/cstring.h" +#include "common/types.h" +#include "core/libraries/ime/ime_common.h" +#include "core/libraries/ime/ime_keyboard_layouts.h" +#include "core/libraries/ime/ime_keyboard_ui.h" +#include "imgui/imgui_std.h" + +using namespace ImGui; + +/** + * Removes one UTF-8 codepoint from the end of 'buffer', if present. + */ +void Utf8SafeBackspace(char* buffer) { + size_t len = std::strlen(buffer); + if (len == 0) + return; + + // Move backward over any continuation bytes. + while (len > 0 && (static_cast(buffer[len]) & 0b11000000) == 0b10000000) { + --len; + } + + if (len > 0) { + // Remove one codepoint. + buffer[len - 1] = '\0'; + buffer[len] = '\0'; + } +} + +/** + * Picks which layout vector we want for OrbisImeType, kb_mode, shift_state, etc. + */ +const std::vector* GetKeyboardLayout(OrbisImeType type, KeyboardMode mode, + ShiftState shift, u64 language) { + switch (type) { + case OrbisImeType::Number: + // For numeric input, you might have a dedicated numeric layout, + // but here we reuse kLayoutEnSymbols1. + return &kLayoutEnSymbols1; + + case OrbisImeType::Url: + case OrbisImeType::Mail: + // Use letters; uppercase if SHIFT is on. + if (shift == ShiftState::CapsLock || shift == ShiftState::Shift) { + return &kLayoutEnLettersUppercase; + } else { + return &kLayoutEnLettersLowercase; + } + + case OrbisImeType::BasicLatin: + case OrbisImeType::Default: + default: + switch (mode) { + case KeyboardMode::Symbols1: + return &kLayoutEnSymbols1; + case KeyboardMode::Symbols2: + return &kLayoutEnSymbols2; + case KeyboardMode::AccentLetters: + if (shift == ShiftState::CapsLock || shift == ShiftState::Shift) { + return &kLayoutEnAccentLettersUppercase; + } else { + return &kLayoutEnAccentLettersLowercase; + } + case KeyboardMode::Letters: + default: + if (shift == ShiftState::CapsLock || shift == ShiftState::Shift) { + return &kLayoutEnLettersUppercase; + } else { + return &kLayoutEnLettersLowercase; + } + } + } +} + +/** + * Renders the given layout using the style logic: + * - For symbols layout and if style.use_button_symbol_color is true, + * character keys get style.color_button_symbol. + * - Function keys get style.color_button_function. + * - The "Done"/"Enter" key (keycode 0x0D) gets style.color_special. + * - Otherwise, keys use style.color_button_default. + * + * This version retains all GUI layout details (positions, colors, sizes, etc.) exactly as in your + * base files. The only change is in key event detection: after drawing each key with Button(), we + * use IsItemActive() to determine the pressed state so that the backend key processing works + * correctly. + */ +void RenderKeyboardLayout(const std::vector& layout, KeyboardMode mode, + void (*on_key_event)(const VirtualKeyEvent*), + const KeyboardStyle& style) { + ImGui::BeginGroup(); + + /* ─────────────── 1. grid size & cell metrics ─────────────── */ + int max_col = 0, max_row = 0; + for (const KeyEntry& k : layout) { + max_col = std::max(max_col, k.col + (int)k.colspan); + max_row = std::max(max_row, k.row + (int)k.rowspan); + } + if (max_col == 0 || max_row == 0) { + ImGui::EndGroup(); + return; + } + + const float pad = 20.0f; + const float spacing_w = (max_col - 1) * style.key_spacing; + const float spacing_h = (max_row - 1) * style.key_spacing; + const float cell_w = std::floor((style.layout_width - spacing_w - 2 * pad) / max_col + 0.5f); + const float cell_h = std::floor((style.layout_height - spacing_h - 85.0f) / max_row + 0.5f); + + ImVec2 origin = ImGui::GetCursorScreenPos(); + origin.x += pad; + + ImGui::PushStyleColor(ImGuiCol_NavHighlight, style.color_line); + ImGui::SetWindowFontScale(1.50f); + + const int function_rows_start = std::max(0, max_row - 2); + + /* ─────────────── 2. draw every key ───────────────────────── */ + for (const KeyEntry& key : layout) { + /* position & size */ + float x = origin.x + key.col * (cell_w + style.key_spacing); + float y = origin.y + key.row * (cell_h + style.key_spacing); + float w = key.colspan * cell_w + (key.colspan - 1) * style.key_spacing; + float h = key.rowspan * cell_h + (key.rowspan - 1) * style.key_spacing; + ImVec2 pos(x, y), size(w, h); + + /* ------------ background colour decision --------------- */ + const bool in_function_rows = (key.row >= function_rows_start); + const bool is_done_enter = (key.keycode == 0x0D); + + ImU32 bg_color; + if (is_done_enter) { + bg_color = style.color_special; // always wins + } else if (in_function_rows) { + bg_color = style.color_button_function; // bottom two rows + } else if ((mode == KeyboardMode::Symbols1 || mode == KeyboardMode::Symbols2) && + style.use_button_symbol_color) { + bg_color = style.color_button_symbol; // symbol tint + } else { + bg_color = style.color_button_default; // normal default + } + + /* label */ + std::string label = (key.label && key.label[0]) ? key.label : " "; + + /* ---------- ImGui button ---------- */ + ImGui::PushID(&key); + ImGui::PushStyleColor(ImGuiCol_Text, style.color_text); + ImGui::PushStyleColor(ImGuiCol_Button, bg_color); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + IM_COL32((bg_color >> IM_COL32_R_SHIFT & 0xFF) * 220 / 255, + (bg_color >> IM_COL32_G_SHIFT & 0xFF) * 220 / 255, + (bg_color >> IM_COL32_B_SHIFT & 0xFF) * 220 / 255, + (bg_color >> IM_COL32_A_SHIFT & 0xFF))); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + IM_COL32((bg_color >> IM_COL32_R_SHIFT & 0xFF) * 180 / 255, + (bg_color >> IM_COL32_G_SHIFT & 0xFF) * 180 / 255, + (bg_color >> IM_COL32_B_SHIFT & 0xFF) * 180 / 255, + (bg_color >> IM_COL32_A_SHIFT & 0xFF))); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + + if (key.allow_repeat) + ImGui::PushButtonRepeat(true); + + ImGui::SetCursorScreenPos(pos); + bool pressed = ImGui::Button(label.c_str(), size); // Down + repeats + + // ——— use ImGui’s built‑in hover highlight ——— + if (ImGui::IsItemHovered()) { + ImGui::SetItemCurrentNavFocus(); + ImGui::KeepNavHighlight(); + } + + if (key.allow_repeat) + ImGui::PopButtonRepeat(); + + /* ---------- event generation ---------- */ + if (on_key_event) { + if (ImGui::IsItemActivated()) { + VirtualKeyEvent ev{VirtualKeyEventType::Down, &key}; + on_key_event(&ev); + } else if (pressed && key.allow_repeat) { + VirtualKeyEvent ev{VirtualKeyEventType::Repeat, &key}; + on_key_event(&ev); + } + + if (ImGui::IsItemDeactivated()) { + VirtualKeyEvent ev{VirtualKeyEventType::Up, &key}; + on_key_event(&ev); + } + } + + /* cleanup */ + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(4); + ImGui::PopID(); + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleColor(); // NavHighlight + ImGui::EndGroup(); +} + +/** + * Selects the correct layout via GetKeyboardLayout() then calls RenderKeyboardLayout(). + */ +void DrawVirtualKeyboard(KeyboardMode kb_mode, OrbisImeType ime_type, ShiftState shift_state, + u64 language, void (*on_key_event)(const VirtualKeyEvent*), + const KeyboardStyle& style) { + const std::vector* layout = + GetKeyboardLayout(ime_type, kb_mode, shift_state, language); + if (!layout) + return; + + RenderKeyboardLayout(*layout, kb_mode, on_key_event, style); +} diff --git a/src/core/libraries/ime/ime_keyboard_ui.h b/src/core/libraries/ime/ime_keyboard_ui.h new file mode 100644 index 000000000..8b1e43745 --- /dev/null +++ b/src/core/libraries/ime/ime_keyboard_ui.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "core/libraries/ime/ime_common.h" +#include "core/libraries/ime/ime_keyboard_layouts.h" + +/** + * KeyboardMode: which layout we show (letters, accents, symbols, etc.) + */ +enum class KeyboardMode { Letters, AccentLetters, Symbols1, Symbols2 }; + +/** + * We handle raw key "Down" or "Up" events from an on-screen keyboard. + */ +enum class VirtualKeyEventType { Down, Up, Repeat }; + +struct VirtualKeyEvent { + VirtualKeyEventType type; + const KeyEntry* key; +}; + +enum class ShiftState : u8 { + None = 0, // lowercase + Shift = 1, // temporary uppercase + CapsLock = 2 // full uppercase +}; + +/** + * This struct holds all visual parameters for the on-screen keyboard, + * including layout size, spacing, and button colors. + * + * If extended parameters are present, it override these defaults + * in IME code. Then pass the result to DrawVirtualKeyboard(...). + */ +struct KeyboardStyle { + float layout_width = 500.0f; + float layout_height = 300.0f; + float key_spacing = 5.0f; + + // For text, lines, etc. + ImU32 color_text = IM_COL32(225, 225, 225, 255); + ImU32 color_line = IM_COL32(88, 88, 88, 255); + + // Button colors + ImU32 color_button_default = IM_COL32(35, 35, 35, 255); + ImU32 color_button_function = IM_COL32(50, 50, 50, 255); + ImU32 color_special = IM_COL32(0, 140, 200, 255); + + // If you're on a symbols layout, you may want to color them differently. + bool use_button_symbol_color = false; + ImU32 color_button_symbol = IM_COL32(60, 60, 60, 255); +}; + +/** + * Safely remove one UTF-8 glyph from the end of 'buffer'. + */ +void Utf8SafeBackspace(char* buffer); + +/** + * Returns the appropriate layout (vector of KeyEntry) for the given + * OrbisImeType, KeyboardMode, ShiftState, and language bitmask. + */ +const std::vector* GetKeyboardLayout(OrbisImeType type, KeyboardMode mode, + ShiftState shift, u64 language); + +/** + * Renders a given layout using the style logic: + * - If 'mode' is a symbols layout (Symbols1 or Symbols2) AND style.use_button_symbol_color == true, + * then normal character keys are drawn with style.color_button_symbol + * - Function keys => style.color_button_function + * - The "Done" or "Enter" key (keycode 0x0D) => style.color_special + * - Otherwise => style.color_button_default + * + * We call on_key_event(...) with VirtualKeyEventType::Down/Up when the user clicks or releases a + * key. + */ +void RenderKeyboardLayout(const std::vector& layout, KeyboardMode mode, + void (*on_key_event)(const VirtualKeyEvent*), const KeyboardStyle& style); + +/** + * Picks the correct layout from GetKeyboardLayout() for the given + * kb_mode, shift_state, etc., then calls RenderKeyboardLayout(). + */ +void DrawVirtualKeyboard(KeyboardMode kb_mode, OrbisImeType ime_type, ShiftState shift_state, + u64 language, void (*on_key_event)(const VirtualKeyEvent*), + const KeyboardStyle& style); \ No newline at end of file diff --git a/src/imgui/imgui_std.h b/src/imgui/imgui_std.h index 743702657..82770258e 100644 --- a/src/imgui/imgui_std.h +++ b/src/imgui/imgui_std.h @@ -88,4 +88,23 @@ static void DrawCenteredText(const char* text, const char* text_end = nullptr, SetCursorPos(pos + content); } +inline void DrawCaretForInputText(const char* text, int caret_index, ImVec2 input_pos, + float padding_x = 4.0f, float padding_y = 3.0f) { + ImVec2 text_size = ImGui::CalcTextSize(text, text + caret_index); + float caret_x = input_pos.x + padding_x + text_size.x; + float caret_y = input_pos.y + padding_y; + + float caret_height = ImGui::GetTextLineHeight(); + + // Optional: make caret blink like ImGui does + float time = ImGui::GetTime(); + bool visible = (fmodf(time, 1.2f) < 0.8f); + if (!visible) + return; + + ImGui::GetWindowDrawList()->AddLine(ImVec2(caret_x, caret_y), + ImVec2(caret_x, caret_y + caret_height), + IM_COL32(255, 255, 255, 255), 1.0f); +} + } // namespace ImGui diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index d143232dc..120c3d0ec 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -69,14 +69,51 @@ void Initialize(const ::Vulkan::Instance& instance, const Frontend::WindowSDL& w rb.AddRanges(io.Fonts->GetGlyphRangesKorean()); rb.AddRanges(io.Fonts->GetGlyphRangesJapanese()); rb.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + // For keyboard + rb.AddChar(U'×'); + rb.AddChar(U'○'); + rb.AddChar(U'△'); + rb.AddChar(U'□'); + rb.AddChar(U'↑'); + rb.AddChar(U'↓'); + rb.AddChar(U'←'); + rb.AddChar(U'→'); + rb.AddChar(U'⊗'); + rb.AddChar(U'⮾'); + rb.AddChar(U'╳'); + rb.AddChar(U'◀'); + rb.AddChar(U'▲'); + rb.AddChar(U'▶'); + rb.AddChar(U'▼'); + rb.AddChar(U'⇧'); + rb.AddChar(U'⬆'); + rb.AddChar(U'⇦'); + rb.AddChar(U'€'); + rb.AddChar(U'₩'); + rb.AddChar(U'“'); + rb.AddChar(U'”'); + rb.AddChar(U'„'); + rb.AddChar(U'‼'); + rb.AddChar(U'¿'); + rb.AddChar(U'⁇'); + rb.AddChar(U'‹'); + rb.AddChar(U'›'); + rb.AddChar(U'’'); + rb.AddChar(U'‘'); + rb.AddChar(U'‛'); + rb.AddChar(U'‚'); + rb.AddChar(U'№'); + ImVector ranges{}; rb.BuildRanges(&ranges); ImFontConfig font_cfg{}; font_cfg.OversampleH = 2; font_cfg.OversampleV = 1; + font_cfg.MergeMode = false; io.FontDefault = io.Fonts->AddFontFromMemoryCompressedTTF( imgui_font_notosansjp_regular_compressed_data, imgui_font_notosansjp_regular_compressed_size, 16.0f, &font_cfg, ranges.Data); + font_cfg.MergeMode = true; io.Fonts->AddFontFromMemoryCompressedTTF(imgui_font_proggyvector_regular_compressed_data, imgui_font_proggyvector_regular_compressed_size, 16.0f);