From d1d104d67b0f0bcc06384500a6fa5002d784fea6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:30:12 +0000 Subject: [PATCH] feat(libSceImeDialog): add the pad keyboard model A pure model of the controller keyboard: the Latin, Symbols and Specials pages with their spanned keys, and a step function from pad samples to edits. It follows shadPS4 30f4170896's on-screen keyboard (ime_ui_shared.cpp:805-887, ime_dialog_ui.cpp and ime_kb_layout.cpp): - Cross activates the selected key, armed once Cross is seen up, and repeats only the editing keys; the D-pad and left stick move on the grid and wrap, a held direction waits half a second at the edge. - The shortcuts run only when the grid did nothing, in the source's order: L2+Triangle symbols, Triangle space (not while L2 is held), L3 specials, R2 done, L1+Square clear, Square backspace, L1 and R1 caret, R3, L2 shift; Circle cancels and loses to Done. - L2 cycles lower, one-shot upper and caps lock, re-armed once the trigger is back at 0.20. - Repeats use ImGui's default delay and rate, 0.275 s and 0.05 s. Every button and stick direction held in the first sample is ignored until it is released, so nothing held when the dialog opens types, deletes or repeats. The keyboard, menu and R3 mode keys of the source are left empty, and the currency key and the Done label follow the dialog's languages and enter label. --- core/libs/prx/libSceImeDialog/CMakeLists.txt | 10 +- .../libSceImeDialog/include/PadKeyboard.hpp | 129 +++ .../prx/libSceImeDialog/src/PadKeyboard.cpp | 860 ++++++++++++++++++ .../prx/libSceImeDialog/tests/PadKeyboard.cpp | 358 ++++++++ 4 files changed, 1356 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceImeDialog/include/PadKeyboard.hpp create mode 100644 core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp create mode 100644 core/libs/prx/libSceImeDialog/tests/PadKeyboard.cpp diff --git a/core/libs/prx/libSceImeDialog/CMakeLists.txt b/core/libs/prx/libSceImeDialog/CMakeLists.txt index 69efa4be3f..e3b25d8fa2 100644 --- a/core/libs/prx/libSceImeDialog/CMakeLists.txt +++ b/core/libs/prx/libSceImeDialog/CMakeLists.txt @@ -1,9 +1,17 @@ -add_library(libSceImeDialog SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +add_library(libSceImeDialog SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadKeyboard.cpp) target_include_directories(libSceImeDialog PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceImeDialog PRIVATE libSceIme) configure_windows_unwind(libSceImeDialog) if(BUILD_TESTING) + add_test_executable(ime_dialog_pad_keyboard_tests tests/PadKeyboard.cpp src/PadKeyboard.cpp) + target_include_directories(ime_dialog_pad_keyboard_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(MINGW) + target_compile_options(ime_dialog_pad_keyboard_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(ime_dialog_pad_keyboard_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_test(NAME ime_dialog_pad_keyboard COMMAND ime_dialog_pad_keyboard_tests) + add_test_executable(guest_ime_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) target_include_directories(guest_ime_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceImeDialog/include/PadKeyboard.hpp b/core/libs/prx/libSceImeDialog/include/PadKeyboard.hpp new file mode 100644 index 0000000000..0b72378a1e --- /dev/null +++ b/core/libs/prx/libSceImeDialog/include/PadKeyboard.hpp @@ -0,0 +1,129 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIMEDIALOG_INCLUDE_PADKEYBOARD_HPP +#define CORE_LIBS_PRX_LIBSCEIMEDIALOG_INCLUDE_PADKEYBOARD_HPP + +#include +#include +#include +#include +#include + +namespace ImeDialog { + +inline constexpr std::uint32_t PadButtonL3 = 0x0002; +inline constexpr std::uint32_t PadButtonR3 = 0x0004; +inline constexpr std::uint32_t PadButtonUp = 0x0010; +inline constexpr std::uint32_t PadButtonRight = 0x0020; +inline constexpr std::uint32_t PadButtonDown = 0x0040; +inline constexpr std::uint32_t PadButtonLeft = 0x0080; +inline constexpr std::uint32_t PadButtonL2 = 0x0100; +inline constexpr std::uint32_t PadButtonR2 = 0x0200; +inline constexpr std::uint32_t PadButtonL1 = 0x0400; +inline constexpr std::uint32_t PadButtonR1 = 0x0800; +inline constexpr std::uint32_t PadButtonTriangle = 0x1000; +inline constexpr std::uint32_t PadButtonCircle = 0x2000; +inline constexpr std::uint32_t PadButtonCross = 0x4000; +inline constexpr std::uint32_t PadButtonSquare = 0x8000; + +inline constexpr double PadKeyRepeatDelay = 0.275; +inline constexpr double PadKeyRepeatRate = 0.050; +inline constexpr double PadStickDeadzone = 0.24; +inline constexpr double PadStickThreshold = 0.50; +inline constexpr double PadTriggerRelease = 0.20; +inline constexpr double PadEdgeWrapHold = 0.5; + +enum class PadKeyAction : std::uint8_t { Character, Shift, Symbols, Specials, Space, Backspace, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, PageNext, PagePrev, Done }; +enum class PadKeyGlyph : std::uint8_t { None, Shift, Backspace, ArrowLeft, ArrowRight, ArrowUp, ArrowDown }; +enum class PadKeyboardFamily : std::uint8_t { Latin, Symbols, Specials }; +enum class PadKeyboardCase : std::uint8_t { Lower, Upper, CapsLock }; +enum class PadKeyboardCurrency : std::uint8_t { Euro, Dollar, Pound, Yen, Won }; +enum class PadEdit : std::uint8_t { None, Insert, Backspace, Clear, CaretLeft, CaretRight, CaretHome, CaretEnd }; +enum class PadFinish : std::uint8_t { None, Done, Cancel }; + +struct PadKey { + std::uint8_t row; + std::uint8_t column; + std::uint8_t columnSpan; + std::uint8_t rowSpan; + const char16_t* label; + PadKeyAction action; + PadKeyGlyph glyph; + const char16_t* badge; +}; + +struct PadSample { + std::uint32_t buttons = 0; + std::uint8_t leftX = 128; + std::uint8_t leftY = 128; + std::uint8_t l2 = 0; +}; + +struct PadKeyboardOptions { + bool controller = true; + bool multiline = false; + PadKeyboardFamily family = PadKeyboardFamily::Latin; + PadKeyboardCase caseState = PadKeyboardCase::Lower; + PadKeyboardCurrency currency = PadKeyboardCurrency::Euro; + std::uint32_t enterLabel = 0; +}; + +struct PadKeyboardResult { + PadEdit edit = PadEdit::None; + std::u16string text; + PadFinish finish = PadFinish::None; +}; + +struct PadRepeat { + bool down = false; + double next = 0.0; +}; + +struct PadKeyboardState { + using Clock = std::chrono::steady_clock; + + PadKeyboardOptions options; + PadKeyboardFamily family = PadKeyboardFamily::Latin; + PadKeyboardFamily alphaFamily = PadKeyboardFamily::Latin; + PadKeyboardCase caseState = PadKeyboardCase::Lower; + std::uint8_t page = 0; + int cursorRow = 0; + int cursorColumn = 0; + int preferColumn = 0; + bool started = false; + bool awaitingInsert = false; + Clock::time_point origin{}; + std::uint32_t heldAtOpen = 0; + bool stickHeldAtOpen = false; + std::uint32_t previous = 0; + bool crossArmed = false; + bool l2Armed = true; + PadRepeat cross; + PadRepeat left; + PadRepeat right; + PadRepeat up; + PadRepeat down; + PadRepeat square; + PadRepeat l1; + PadRepeat r1; + double triangleNext = 0.0; + std::array previousStick{}; + int stickDirection = 0; + double stickNext = 0.0; + bool wrapHold = false; + int wrapRow = 0; + int wrapColumn = 0; + double wrapRelease = 0.0; +}; + +PadKeyboardState PadKeyboardOpen(const PadKeyboardOptions& options); +PadKeyboardResult PadKeyboardStep(PadKeyboardState& state, const PadSample& sample, PadKeyboardState::Clock::time_point now); +void PadKeyboardNoteInsert(PadKeyboardState& state, bool inserted); +std::span PadKeyboardKeys(const PadKeyboardState& state); +std::uint8_t PadKeyboardRows(const PadKeyboardState& state); +std::uint8_t PadKeyboardColumns(); +const PadKey* PadKeyboardSelected(const PadKeyboardState& state); +std::u16string PadKeyboardLabel(const PadKeyboardState& state, const PadKey& key); +PadKeyboardCurrency PadKeyboardCurrencyFor(std::uint64_t supportedLanguages); + +} + +#endif diff --git a/core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp b/core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp new file mode 100644 index 0000000000..e14eeb3db3 --- /dev/null +++ b/core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp @@ -0,0 +1,860 @@ +#include "prx/libSceImeDialog/include/PadKeyboard.hpp" + +#include +#include +#include +#include + +namespace ImeDialog { + +namespace { + +constexpr PadKey LatinLowerKeys[] = { + {0, 0, 1, 1, u"1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u"8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"q", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"w", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u"e", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u"r", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u"t", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u"y", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"u", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"i", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"o", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"p", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"a", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u"s", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"d", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"f", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"g", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"h", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"j", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"k", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"l", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 1, u"'", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"z", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"x", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"c", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"v", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"b", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"n", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 6, 1, 1, u"m", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 7, 1, 1, u",", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 8, 1, 1, u".", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 9, 1, 1, u"?", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 0, 1, 1, nullptr, PadKeyAction::Shift, PadKeyGlyph::Shift, u"L2"}, + {4, 1, 1, 1, u"@#:", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {4, 2, 1, 1, u"\u00E0", PadKeyAction::Specials, PadKeyGlyph::None, u"L3"}, + {4, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {4, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {5, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {5, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {5, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {5, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr PadKey LatinUpperKeys[] = { + {0, 0, 1, 1, u"1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u"8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"Q", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"W", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u"E", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u"R", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u"T", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u"Y", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"U", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"I", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"O", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"P", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"A", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u"S", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"D", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"F", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"G", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"H", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"J", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"K", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"L", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 1, u"\"", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"Z", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"X", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"C", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"V", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"B", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"N", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 6, 1, 1, u"M", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 7, 1, 1, u"-", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 8, 1, 1, u"_", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 9, 1, 1, u"/", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 0, 1, 1, nullptr, PadKeyAction::Shift, PadKeyGlyph::Shift, u"L2"}, + {4, 1, 1, 1, u"@#:", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {4, 2, 1, 1, u"\u00E0", PadKeyAction::Specials, PadKeyGlyph::None, u"L3"}, + {4, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {4, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {5, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {5, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {5, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {5, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr PadKey SymbolsFirstKeys[] = { + {0, 0, 1, 1, u"!", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"?", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"\"", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"'", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"#", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"%", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"(", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u")", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"()", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"/", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"-", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"_", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u",", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u".", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u":", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u";", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"*", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"+", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"=", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"&", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"<", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u">", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"@", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"[", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"]", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"[]", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"{", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"}", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"{}", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 2, u">", PadKeyAction::PageNext, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"\\", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"|", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"^", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"`", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"$", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"\u20AC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 6, 1, 1, u"\u00B4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 7, 1, 1, u"\u2018", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 8, 1, 1, u"\u2019", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 1, 1, 1, u"ABC", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {4, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {4, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {5, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {5, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {5, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {5, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr PadKey SymbolsSecondKeys[] = { + {0, 0, 1, 1, u"\u201A", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"\u201C", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"\u201D", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"\u201E", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"~", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"\u00A1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"\u00A1!", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u"\u00BF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"\u00BF?", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"\u2039", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"\u203A", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"\u00AB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u"\u00BB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u"\u00B0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u"\u00AA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u"\u00BA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"\u00D7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"\u00F7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"\u00A4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"\u00A2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"\u00A5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u"\u00A3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"\u20A9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"\u00A7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"\u00A6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"\u00B5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"\u00AC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"\u00B9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"\u00B2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 2, u"<", PadKeyAction::PagePrev, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"\u00B3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"\u00BC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"\u00BD", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"\u00BE", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"\u2116", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"\u00B7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 1, 1, 1, u"ABC", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {4, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {4, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {5, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {5, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {5, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {5, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr PadKey SpecialsLowerKeys[] = { + {0, 0, 1, 1, u"\u00E0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"\u00E1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"\u00E2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"\u00E3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"\u00E4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"\u00E5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"\u0105", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u"\u00E6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"\u00E7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"\u0107", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"\u00E8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"\u00E9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u"\u00EA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u"\u00EB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u"\u0119", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u"\u011F", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"\u00EC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"\u00ED", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"\u00EE", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"\u00EF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"\u0131", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u"\u0142", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"\u00F1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"\u0144", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"\u00F2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"\u00F3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"\u00F4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"\u00F5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"\u00F6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 1, u"\u00F8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"\u0153", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"\u015B", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"\u015F", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"\u0161", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"\u00DF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"\u00F9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 6, 1, 1, u"\u00FA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 7, 1, 1, u"\u00FB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 8, 1, 1, u"\u00FC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 9, 1, 1, u"\u00FD", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 0, 1, 1, u"\u00FF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 1, 1, 1, u"\u017A", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 2, 1, 1, u"\u017C", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 3, 1, 1, u"\u017E", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 4, 1, 1, u"\u00F0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 5, 1, 1, u"\u00FE", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {5, 0, 1, 1, nullptr, PadKeyAction::Shift, PadKeyGlyph::Shift, u"L2"}, + {5, 1, 1, 1, u"@#:", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {5, 2, 1, 1, u"ABC", PadKeyAction::Specials, PadKeyGlyph::None, u"L3"}, + {5, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {6, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {6, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {6, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {6, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {6, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr PadKey SpecialsUpperKeys[] = { + {0, 0, 1, 1, u"\u00C0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 1, 1, 1, u"\u00C1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 2, 1, 1, u"\u00C2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 3, 1, 1, u"\u00C3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 4, 1, 1, u"\u00C4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 5, 1, 1, u"\u00C5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 6, 1, 1, u"\u0104", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 7, 1, 1, u"\u00C6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 8, 1, 1, u"\u00C7", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {0, 9, 1, 1, u"\u0106", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 0, 1, 1, u"\u00C8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 1, 1, 1, u"\u00C9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 2, 1, 1, u"\u00CA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 3, 1, 1, u"\u00CB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 4, 1, 1, u"\u0118", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 5, 1, 1, u"\u011E", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 6, 1, 1, u"\u00CC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 7, 1, 1, u"\u00CD", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 8, 1, 1, u"\u00CE", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {1, 9, 1, 1, u"\u00CF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 0, 1, 1, u"\u0130", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 1, 1, 1, u"\u0141", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 2, 1, 1, u"\u00D1", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 3, 1, 1, u"\u0143", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 4, 1, 1, u"\u00D2", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 5, 1, 1, u"\u00D3", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 6, 1, 1, u"\u00D4", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 7, 1, 1, u"\u00D5", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 8, 1, 1, u"\u00D6", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {2, 9, 1, 1, u"\u00D8", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 0, 1, 1, u"\u0152", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 1, 1, 1, u"\u015A", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 2, 1, 1, u"\u015E", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 3, 1, 1, u"\u0160", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 4, 1, 1, u"\u00DF", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 5, 1, 1, u"\u00D9", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 6, 1, 1, u"\u00DA", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 7, 1, 1, u"\u00DB", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 8, 1, 1, u"\u00DC", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {3, 9, 1, 1, u"\u00DD", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 0, 1, 1, u"\u0178", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 1, 1, 1, u"\u0179", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 2, 1, 1, u"\u017B", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 3, 1, 1, u"\u017D", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 4, 1, 1, u"\u00D0", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {4, 5, 1, 1, u"\u00DE", PadKeyAction::Character, PadKeyGlyph::None, nullptr}, + {5, 0, 1, 1, nullptr, PadKeyAction::Shift, PadKeyGlyph::Shift, u"L2"}, + {5, 1, 1, 1, u"@#:", PadKeyAction::Symbols, PadKeyGlyph::None, u"L2+\u25B3"}, + {5, 2, 1, 1, u"ABC", PadKeyAction::Specials, PadKeyGlyph::None, u"L3"}, + {5, 3, 4, 1, u"Space", PadKeyAction::Space, PadKeyGlyph::None, u"\u25B3"}, + {5, 8, 2, 1, nullptr, PadKeyAction::Backspace, PadKeyGlyph::Backspace, u"\u25A1"}, + {6, 0, 1, 1, nullptr, PadKeyAction::ArrowDown, PadKeyGlyph::ArrowDown, nullptr}, + {6, 1, 1, 1, nullptr, PadKeyAction::ArrowUp, PadKeyGlyph::ArrowUp, nullptr}, + {6, 2, 1, 1, nullptr, PadKeyAction::ArrowLeft, PadKeyGlyph::ArrowLeft, u"L1"}, + {6, 3, 1, 1, nullptr, PadKeyAction::ArrowRight, PadKeyGlyph::ArrowRight, u"R1"}, + {6, 8, 2, 1, nullptr, PadKeyAction::Done, PadKeyGlyph::None, u"R2"}, +}; + +constexpr std::uint8_t Columns = 10; +constexpr std::uint64_t LanguageEnglishUs = 0x0000000000000004; +constexpr std::uint64_t LanguageJapanese = 0x0000000000002000; +constexpr std::uint64_t LanguageKorean = 0x0000000000004000; +constexpr std::uint64_t LanguagePortugueseBr = 0x0000000000020000; +constexpr std::uint64_t LanguageEnglishGb = 0x0000000000040000; +constexpr std::uint64_t LanguageSpanishLa = 0x0000000000100000; + +struct Layout { + std::span keys; + std::uint8_t rows; +}; + +struct Repeated { + bool pressed = false; + bool repeat = false; +}; + +struct StickPulse { + bool left = false; + bool right = false; + bool up = false; + bool down = false; + bool repeat = false; +}; + +Layout layoutOf(PadKeyboardFamily family, PadKeyboardCase caseState, std::uint8_t page) { + switch (family) { + case PadKeyboardFamily::Symbols: + return page == 0 ? Layout{SymbolsFirstKeys, 6} : Layout{SymbolsSecondKeys, 6}; + case PadKeyboardFamily::Specials: + return caseState == PadKeyboardCase::Lower ? Layout{SpecialsLowerKeys, 7} : Layout{SpecialsUpperKeys, 7}; + case PadKeyboardFamily::Latin: + break; + } + return caseState == PadKeyboardCase::Lower ? Layout{LatinLowerKeys, 6} : Layout{LatinUpperKeys, 6}; +} + +Layout layoutOf(const PadKeyboardState& state) { + return layoutOf(state.family, state.caseState, state.page); +} + +const PadKey* keyAt(const Layout& layout, int row, int column) { + if (row < 0 || column < 0 || row >= layout.rows || column >= Columns) return nullptr; + for (const auto& key : layout.keys) { + if (row >= key.row && row < key.row + key.rowSpan && column >= key.column && column < key.column + key.columnSpan) return &key; + } + return nullptr; +} + +std::pair nearestOnRow(const Layout& layout, int row, int column, int prefer) { + if (row < 0 || row >= layout.rows) return {-1, -1}; + int best = -1; + int bestDistance = std::numeric_limits::max(); + bool bestInDirection = false; + for (int candidate = 0; candidate < Columns; ++candidate) { + if (keyAt(layout, row, candidate) == nullptr) continue; + const int distance = std::abs(candidate - column); + const bool inDirection = (prefer > 0 && candidate > column) || (prefer < 0 && candidate < column); + const bool betterTie = prefer != 0 && inDirection != bestInDirection ? inDirection : candidate < best; + if (distance < bestDistance || (distance == bestDistance && (best < 0 || betterTie))) { + bestDistance = distance; + best = candidate; + bestInDirection = inDirection; + } + } + return best >= 0 ? std::pair{row, best} : std::pair{-1, -1}; +} + +std::pair nearest(const Layout& layout, int row, int column) { + std::pair best{-1, -1}; + int bestDistance = std::numeric_limits::max(); + for (int candidateRow = 0; candidateRow < layout.rows; ++candidateRow) { + for (int candidate = 0; candidate < Columns; ++candidate) { + if (keyAt(layout, candidateRow, candidate) == nullptr) continue; + const int distance = std::abs(candidateRow - row) + std::abs(candidate - column); + if (distance < bestDistance) { + bestDistance = distance; + best = {candidateRow, candidate}; + } + } + } + return best; +} + +std::pair visibleCell(const PadKeyboardState& state) { + const auto layout = layoutOf(state); + if (keyAt(layout, state.cursorRow, state.cursorColumn) != nullptr) return {state.cursorRow, state.cursorColumn}; + const auto onRow = nearestOnRow(layout, state.cursorRow, state.cursorColumn, state.preferColumn); + if (onRow.first >= 0) return onRow; + return nearest(layout, std::clamp(state.cursorRow, 0, layout.rows - 1), std::clamp(state.cursorColumn, 0, Columns - 1)); +} + +Repeated repeatButton(bool down, bool edge, double now, PadRepeat& repeat) { + Repeated result; + if (!down) { + repeat = {}; + return result; + } + if (!repeat.down) { + repeat.down = true; + repeat.next = now + PadKeyRepeatDelay; + result.pressed = edge; + return result; + } + if (edge) { + repeat.next = now + PadKeyRepeatDelay; + result.pressed = true; + return result; + } + if (now >= repeat.next) { + repeat.next = now + PadKeyRepeatRate; + result.pressed = true; + result.repeat = true; + } + return result; +} + +double axis(std::uint8_t value) { + const double centred = std::clamp((static_cast(value) - 128.0) / 127.0, -1.0, 1.0); + return std::abs(centred) < PadStickDeadzone ? 0.0 : centred; +} + +std::array stickDirections(double x, double y) { + return {x <= -PadStickThreshold, x >= PadStickThreshold, y <= -PadStickThreshold, y >= PadStickThreshold}; +} + +StickPulse stickPulse(PadKeyboardState& state, double x, double y, const std::array& edges, double now) { + StickPulse result; + const double absX = std::abs(x); + const double absY = std::abs(y); + int direction = 0; + double strength = 0.0; + if (absX >= PadStickThreshold || absY >= PadStickThreshold) { + if (absX >= absY) { + strength = absX; + direction = x < 0.0 ? 1 : 2; + } else { + strength = absY; + direction = y < 0.0 ? 3 : 4; + } + } + if (direction == 0) { + state.stickDirection = 0; + state.stickNext = 0.0; + return result; + } + const double tilt = std::clamp((strength - PadStickThreshold) / (1.0 - PadStickThreshold), 0.0, 1.0); + const double initialDelay = std::lerp(0.26, 0.16, tilt); + const double repeatInterval = std::lerp(0.20, 0.11, tilt); + const bool edge = edges[static_cast(direction - 1)]; + bool pulse = false; + if (state.stickDirection != direction) { + const bool fastRotate = state.stickDirection != 0 && now < state.stickNext; + pulse = state.stickDirection == 0 || edge || fastRotate; + state.stickDirection = direction; + state.stickNext = now + initialDelay; + } else if (edge) { + pulse = true; + state.stickNext = now + initialDelay; + } else if (now >= state.stickNext) { + pulse = true; + result.repeat = true; + state.stickNext = now + repeatInterval; + } + if (!pulse) return result; + result.left = direction == 1; + result.right = direction == 2; + result.up = direction == 3; + result.down = direction == 4; + return result; +} + +bool delayWrap(PadKeyboardState& state, int stepRow, int stepColumn, bool repeatHint, bool wraps, double now) { + if (state.wrapHold && (state.wrapRow != stepRow || state.wrapColumn != stepColumn)) state.wrapHold = false; + if (!wraps) return false; + const bool sameDirection = state.wrapHold && state.wrapRow == stepRow && state.wrapColumn == stepColumn; + if (!repeatHint && !sameDirection) return false; + if (!sameDirection) { + state.wrapHold = true; + state.wrapRow = stepRow; + state.wrapColumn = stepColumn; + state.wrapRelease = now + PadEdgeWrapHold; + } + return now < state.wrapRelease; +} + +void move(PadKeyboardState& state, int stepRow, int stepColumn, bool repeatHint, double now) { + const auto layout = layoutOf(state); + int row = state.cursorRow; + int column = state.cursorColumn; + const PadKey* origin = keyAt(layout, row, column); + bool wrapped = false; + for (int step = 0; step < layout.rows * Columns; ++step) { + const int nextRow = row + stepRow; + const int nextColumn = column + stepColumn; + wrapped = wrapped || nextRow < 0 || nextRow >= layout.rows || nextColumn < 0 || nextColumn >= Columns; + row = (nextRow + layout.rows) % layout.rows; + column = (nextColumn + Columns) % Columns; + const PadKey* candidate = keyAt(layout, row, column); + if (candidate == nullptr || candidate == origin) continue; + if (delayWrap(state, stepRow, stepColumn, repeatHint, wrapped, now)) return; + state.cursorRow = row; + state.cursorColumn = column; + state.preferColumn = stepColumn; + return; + } +} + +void changeLayout(PadKeyboardState& state, PadKeyboardFamily family, PadKeyboardCase caseState, std::uint8_t page) { + if (state.family == family && state.caseState == caseState && state.page == page) return; + state.family = family; + state.caseState = caseState; + state.page = page; + state.wrapHold = false; + const auto layout = layoutOf(state); + if (state.cursorRow < 0 || state.cursorRow >= layout.rows || state.cursorColumn < 0 || state.cursorColumn >= Columns) { + const auto anchor = nearest(layout, std::clamp(state.cursorRow, 0, layout.rows - 1), std::clamp(state.cursorColumn, 0, Columns - 1)); + state.cursorRow = anchor.first; + state.cursorColumn = anchor.second; + } +} + +void cycleCase(PadKeyboardState& state) { + const auto next = state.caseState == PadKeyboardCase::Lower ? PadKeyboardCase::Upper : state.caseState == PadKeyboardCase::Upper ? PadKeyboardCase::CapsLock : PadKeyboardCase::Lower; + changeLayout(state, state.family, next, state.page); +} + +void toggleFamily(PadKeyboardState& state, PadKeyboardFamily target) { + const auto set = [&](PadKeyboardFamily family) { + if (family == PadKeyboardFamily::Latin || family == PadKeyboardFamily::Specials) state.alphaFamily = family; + changeLayout(state, family, state.caseState, 0); + }; + if (target == PadKeyboardFamily::Symbols) { + if (state.family == PadKeyboardFamily::Symbols) { + set(state.alphaFamily); + } else { + state.alphaFamily = state.family; + set(PadKeyboardFamily::Symbols); + } + return; + } + set(state.family == target ? PadKeyboardFamily::Latin : target); +} + +void focusAction(PadKeyboardState& state, PadKeyAction action) { + const auto layout = layoutOf(state); + for (const auto& key : layout.keys) { + if (key.action != action) continue; + auto target = nearestOnRow(layout, key.row, key.column, 0); + if (target.first < 0) target = nearest(layout, key.row, key.column); + if (target.first < 0) return; + state.cursorRow = target.first; + state.cursorColumn = target.second; + state.preferColumn = 0; + state.wrapHold = false; + return; + } +} + +void flipPage(PadKeyboardState& state, int direction) { + if (state.family != PadKeyboardFamily::Symbols && state.family != PadKeyboardFamily::Specials) return; + changeLayout(state, state.family, state.caseState, static_cast((state.page + direction + 2) % 2)); +} + +bool allowsRepeat(PadKeyAction action) { + switch (action) { + case PadKeyAction::Character: + case PadKeyAction::Space: + case PadKeyAction::Backspace: + case PadKeyAction::ArrowLeft: + case PadKeyAction::ArrowRight: + case PadKeyAction::ArrowUp: + case PadKeyAction::ArrowDown: + return true; + default: + return false; + } +} + +const char16_t* currencyLabel(PadKeyboardCurrency currency) { + switch (currency) { + case PadKeyboardCurrency::Dollar: return u"$"; + case PadKeyboardCurrency::Pound: return u"£"; + case PadKeyboardCurrency::Yen: return u"¥"; + case PadKeyboardCurrency::Won: return u"₩"; + case PadKeyboardCurrency::Euro: break; + } + return u"€"; +} + +const char16_t* enterLabel(std::uint32_t label) { + switch (label) { + case 1: return u"Send"; + case 2: return u"Search"; + case 3: return u"Go"; + default: return u"Done"; + } +} + +void applyKey(PadKeyboardState& state, const PadKey& key, PadKeyboardResult& result) { + switch (key.action) { + case PadKeyAction::Character: + result.edit = PadEdit::Insert; + result.text = PadKeyboardLabel(state, key); + state.awaitingInsert = true; + return; + case PadKeyAction::Shift: + cycleCase(state); + return; + case PadKeyAction::Symbols: + toggleFamily(state, PadKeyboardFamily::Symbols); + focusAction(state, PadKeyAction::Symbols); + return; + case PadKeyAction::Specials: + toggleFamily(state, PadKeyboardFamily::Specials); + focusAction(state, PadKeyAction::Specials); + return; + case PadKeyAction::Space: + result.edit = PadEdit::Insert; + result.text = u" "; + return; + case PadKeyAction::Backspace: + result.edit = PadEdit::Backspace; + return; + case PadKeyAction::ArrowLeft: + result.edit = PadEdit::CaretLeft; + return; + case PadKeyAction::ArrowRight: + result.edit = PadEdit::CaretRight; + return; + case PadKeyAction::ArrowUp: + if (!state.options.multiline) result.edit = PadEdit::CaretHome; + return; + case PadKeyAction::ArrowDown: + if (!state.options.multiline) result.edit = PadEdit::CaretEnd; + return; + case PadKeyAction::PageNext: + flipPage(state, 1); + return; + case PadKeyAction::PagePrev: + flipPage(state, -1); + return; + case PadKeyAction::Done: + result.finish = PadFinish::Done; + return; + } +} + +} + +PadKeyboardState PadKeyboardOpen(const PadKeyboardOptions& options) { + PadKeyboardState state; + state.options = options; + state.family = options.family; + state.alphaFamily = options.family == PadKeyboardFamily::Symbols ? PadKeyboardFamily::Latin : options.family; + state.caseState = options.family == PadKeyboardFamily::Symbols ? PadKeyboardCase::Lower : options.caseState; + const bool latinLower = state.family == PadKeyboardFamily::Latin && state.caseState == PadKeyboardCase::Lower; + const auto layout = layoutOf(state); + auto start = latinLower ? nearestOnRow(layout, 2, 4, 0) : nearest(layout, 0, 0); + state.cursorRow = start.first; + state.cursorColumn = start.second; + return state; +} + +PadKeyboardResult PadKeyboardStep(PadKeyboardState& state, const PadSample& sample, PadKeyboardState::Clock::time_point now) { + PadKeyboardResult result; + const double x = axis(sample.leftX); + const double y = axis(sample.leftY); + auto directions = stickDirections(x, y); + const bool deflected = directions[0] || directions[1] || directions[2] || directions[3]; + if (!state.started) { + state.started = true; + state.origin = now; + state.heldAtOpen = sample.buttons; + state.stickHeldAtOpen = deflected; + state.previous = 0; + state.previousStick = {}; + } + const double time = std::chrono::duration(now - state.origin).count(); + state.heldAtOpen &= sample.buttons; + const std::uint32_t buttons = sample.buttons & ~state.heldAtOpen; + if (state.stickHeldAtOpen && !deflected) state.stickHeldAtOpen = false; + const bool stickUsable = !state.stickHeldAtOpen; + if (!stickUsable) directions = {}; + const auto down = [&](std::uint32_t mask) { return (buttons & mask) != 0; }; + const auto pressed = [&](std::uint32_t mask) { return (buttons & mask) != 0 && (state.previous & mask) == 0; }; + const auto commit = [&] { + state.previous = buttons; + state.previousStick = directions; + }; + if (!state.crossArmed && !down(PadButtonCross)) state.crossArmed = true; + if (!state.options.controller) { + state.square = state.l1 = state.r1 = {}; + state.l2Armed = true; + state.triangleNext = 0.0; + state.stickDirection = 0; + state.stickNext = 0.0; + commit(); + return result; + } + const auto crossRepeat = repeatButton(down(PadButtonCross), pressed(PadButtonCross), time, state.cross); + const auto left = repeatButton(down(PadButtonLeft), pressed(PadButtonLeft), time, state.left); + const auto right = repeatButton(down(PadButtonRight), pressed(PadButtonRight), time, state.right); + const auto upward = repeatButton(down(PadButtonUp), pressed(PadButtonUp), time, state.up); + const auto downward = repeatButton(down(PadButtonDown), pressed(PadButtonDown), time, state.down); + std::array stickEdges{}; + for (std::size_t i = 0; i < 4; ++i) stickEdges[i] = directions[i] && !state.previousStick[i]; + StickPulse stick; + if (stickUsable) { + stick = stickPulse(state, x, y, stickEdges, time); + } else { + state.stickDirection = 0; + state.stickNext = 0.0; + } + const bool navLeft = left.pressed || stick.left; + const bool navRight = right.pressed || stick.right; + const bool navUp = upward.pressed || stick.up; + const bool navDown = downward.pressed || stick.down; + if (navLeft) { + move(state, 0, -1, left.repeat || (stick.left && stick.repeat), time); + } else if (navRight) { + move(state, 0, 1, right.repeat || (stick.right && stick.repeat), time); + } else if (navUp) { + move(state, -1, 0, upward.repeat || (stick.up && stick.repeat), time); + } else if (navDown) { + move(state, 1, 0, downward.repeat || (stick.down && stick.repeat), time); + } + const bool activatePressed = state.crossArmed && (pressed(PadButtonCross) || crossRepeat.repeat); + bool activated = false; + if (activatePressed) { + const auto [row, column] = visibleCell(state); + if (const PadKey* key = keyAt(layoutOf(state), row, column); key != nullptr) { + const bool repeat = crossRepeat.repeat; + if (!repeat || allowsRepeat(key->action)) { + applyKey(state, *key, result); + activated = true; + } + } + } + if (!activated) { + const bool l1Down = down(PadButtonL1); + const bool l1Edge = pressed(PadButtonL1); + const bool l1Repeat = repeatButton(l1Down, l1Edge, time, state.l1).pressed; + const bool squareDown = down(PadButtonSquare); + const bool squareEdge = pressed(PadButtonSquare); + const bool squareRepeat = repeatButton(squareDown, squareEdge, time, state.square).pressed; + const bool r1Repeat = repeatButton(down(PadButtonR1), pressed(PadButtonR1), time, state.r1).pressed; + const bool clearAll = (l1Down && squareEdge) || (squareDown && l1Edge); + const bool l2Down = down(PadButtonL2); + const bool l2Edge = pressed(PadButtonL2); + const double l2Analog = std::max(static_cast(sample.l2) / 255.0, l2Down ? 1.0 : 0.0); + if (!l2Down && l2Analog <= PadTriggerRelease) state.l2Armed = true; + bool l2Pressed = false; + if (state.l2Armed && l2Edge) { + state.l2Armed = false; + l2Pressed = true; + } + const bool triangleDown = down(PadButtonTriangle); + const bool triangleEdge = pressed(PadButtonTriangle); + PadRepeat triangle{(state.previous & PadButtonTriangle) != 0, state.triangleNext}; + const bool trianglePressed = repeatButton(triangleDown, triangleEdge, time, triangle).pressed; + state.triangleNext = triangle.next; + const bool space = trianglePressed && !l2Down; + const bool symbols = (l2Down && triangleEdge) || (triangleDown && l2Pressed); + if (symbols) { + toggleFamily(state, PadKeyboardFamily::Symbols); + } else if (space) { + result.edit = PadEdit::Insert; + result.text = u" "; + } else if (state.family != PadKeyboardFamily::Symbols && pressed(PadButtonL3)) { + toggleFamily(state, PadKeyboardFamily::Specials); + } else if (pressed(PadButtonR2)) { + result.finish = PadFinish::Done; + } else if (clearAll) { + result.edit = PadEdit::Clear; + } else if (squareRepeat) { + result.edit = PadEdit::Backspace; + } else if (l1Repeat) { + result.edit = PadEdit::CaretLeft; + } else if (r1Repeat) { + result.edit = PadEdit::CaretRight; + } else if (!pressed(PadButtonR3) && state.family != PadKeyboardFamily::Symbols && l2Pressed) { + cycleCase(state); + } + } + if (result.finish == PadFinish::None && pressed(PadButtonCircle)) result.finish = PadFinish::Cancel; + commit(); + return result; +} + +void PadKeyboardNoteInsert(PadKeyboardState& state, bool inserted) { + const bool typed = std::exchange(state.awaitingInsert, false); + if (typed && inserted && state.caseState == PadKeyboardCase::Upper) changeLayout(state, state.family, PadKeyboardCase::Lower, state.page); +} + +std::span PadKeyboardKeys(const PadKeyboardState& state) { + return layoutOf(state).keys; +} + +std::uint8_t PadKeyboardRows(const PadKeyboardState& state) { + return layoutOf(state).rows; +} + +std::uint8_t PadKeyboardColumns() { + return Columns; +} + +const PadKey* PadKeyboardSelected(const PadKeyboardState& state) { + const auto [row, column] = visibleCell(state); + return keyAt(layoutOf(state), row, column); +} + +std::u16string PadKeyboardLabel(const PadKeyboardState& state, const PadKey& key) { + if (key.action == PadKeyAction::Done) return enterLabel(state.options.enterLabel); + if (key.action == PadKeyAction::Character && state.family == PadKeyboardFamily::Symbols && state.page == 0 && key.row == 3 && key.column == 5) return currencyLabel(state.options.currency); + return key.label != nullptr ? std::u16string(key.label) : std::u16string(); +} + +PadKeyboardCurrency PadKeyboardCurrencyFor(std::uint64_t supportedLanguages) { + if ((supportedLanguages & LanguageKorean) != 0) return PadKeyboardCurrency::Won; + if ((supportedLanguages & LanguageJapanese) != 0) return PadKeyboardCurrency::Yen; + if ((supportedLanguages & LanguageEnglishGb) != 0) return PadKeyboardCurrency::Pound; + if ((supportedLanguages & (LanguageEnglishUs | LanguageSpanishLa | LanguagePortugueseBr)) != 0) return PadKeyboardCurrency::Dollar; + return PadKeyboardCurrency::Euro; +} + +} diff --git a/core/libs/prx/libSceImeDialog/tests/PadKeyboard.cpp b/core/libs/prx/libSceImeDialog/tests/PadKeyboard.cpp new file mode 100644 index 0000000000..efc3ce2293 --- /dev/null +++ b/core/libs/prx/libSceImeDialog/tests/PadKeyboard.cpp @@ -0,0 +1,358 @@ +#include "prx/libSceImeDialog/include/PadKeyboard.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ImeDialog; +using Clock = PadKeyboardState::Clock; + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +struct Driver { + explicit Driver(PadKeyboardOptions options = {}) : state(PadKeyboardOpen(options)) {} + + PadKeyboardResult Step(std::uint32_t buttons, std::uint8_t leftX = 128, std::uint8_t leftY = 128, std::uint8_t l2 = 0, double seconds = 0.016) { + now += std::chrono::duration_cast(std::chrono::duration(seconds)); + return PadKeyboardStep(state, {buttons, leftX, leftY, l2}, now); + } + + PadKeyboardResult Press(std::uint32_t button) { + auto result = Step(button); + Step(0); + return result; + } + + std::u16string SelectedLabel() const { + const PadKey* key = PadKeyboardSelected(state); + check(key != nullptr, "a key is selected"); + return PadKeyboardLabel(state, *key); + } + + std::u16string FirstLabel() const { + return PadKeyboardLabel(state, PadKeyboardKeys(state).front()); + } + + std::vector Hold(std::uint32_t buttons, double seconds) { + std::vector results; + for (double elapsed = 0.0; elapsed < seconds; elapsed += 0.01) results.push_back(Step(buttons, 128, 128, (buttons & PadButtonL2) != 0 ? 255 : 0, 0.01)); + return results; + } + + PadKeyboardState state; + Clock::time_point now{}; +}; + +std::size_t count(const std::vector& results, PadEdit edit) { + std::size_t total = 0; + for (const auto& result : results) total += result.edit == edit ? 1 : 0; + return total; +} + +void startsOnG() { + Driver pad; + pad.Step(0); + check(pad.SelectedLabel() == u"g", "the Latin keyboard opens on g"); + const auto typed = pad.Press(PadButtonCross); + check(typed.edit == PadEdit::Insert && typed.text == u"g", "Cross types the selected key"); +} + +void heldAtOpenDoesNothing() { + for (const std::uint32_t button : {PadButtonCross, PadButtonSquare, PadButtonRight, PadButtonTriangle, PadButtonL1, PadButtonR2, PadButtonCircle}) { + Driver pad; + const auto held = pad.Hold(button, 1.0); + for (const auto& result : held) check(result.edit == PadEdit::None && result.finish == PadFinish::None, "a button held when the dialog opens does nothing while held"); + check(pad.SelectedLabel() == u"g", "a direction held at open does not move the selection"); + pad.Step(0); + const auto again = pad.Step(button); + check(again.edit != PadEdit::None || again.finish != PadFinish::None || pad.SelectedLabel() != u"g", "the button acts once released and pressed again"); + } + Driver pad; + for (int i = 0; i < 60; ++i) pad.Step(0, 255, 128); + check(pad.SelectedLabel() == u"g", "a stick held at open does not move the selection"); + pad.Step(0); + pad.Step(0, 255, 128); + check(pad.SelectedLabel() == u"h", "the stick moves once centred and pushed again"); +} + +void finishes() { + Driver done; + done.Step(0); + check(done.Press(PadButtonR2).finish == PadFinish::Done, "R2 is Done"); + Driver cancel; + cancel.Step(0); + check(cancel.Press(PadButtonCircle).finish == PadFinish::Cancel, "Circle cancels"); + Driver both; + both.Step(0); + check(both.Press(PadButtonR2 | PadButtonCircle).finish == PadFinish::Done, "Done wins over Cancel in one step"); + Driver key; + key.Step(0); + key.state.cursorRow = 5; + key.state.cursorColumn = 8; + check(key.SelectedLabel() == u"Done", "the Done key carries the default enter label"); + check(key.Press(PadButtonCross).finish == PadFinish::Done, "Cross on the Done key finishes"); + Driver go({true, false, PadKeyboardFamily::Latin, PadKeyboardCase::Lower, PadKeyboardCurrency::Euro, 3}); + go.state.cursorRow = 5; + go.state.cursorColumn = 9; + check(go.SelectedLabel() == u"Go", "the enter label names the Done key"); +} + +void spaceAndBackspace() { + Driver pad; + pad.Step(0); + const auto space = pad.Press(PadButtonTriangle); + check(space.edit == PadEdit::Insert && space.text == u" ", "Triangle types a space"); + const auto backspace = pad.Press(PadButtonSquare); + check(backspace.edit == PadEdit::Backspace, "Square is backspace"); + pad.Step(0); + const auto held = pad.Hold(PadButtonSquare, 0.5); + const auto repeats = count(held, PadEdit::Backspace); + check(repeats >= 4 && repeats <= 7, "Square repeats after 0.275 s every 0.05 s: " + std::to_string(repeats)); + pad.Step(0); + const auto spaces = count(pad.Hold(PadButtonTriangle, 0.5), PadEdit::Insert); + check(spaces >= 4 && spaces <= 7, "Triangle repeats like Square: " + std::to_string(spaces)); +} + +void caret() { + Driver pad; + pad.Step(0); + check(pad.Press(PadButtonL1).edit == PadEdit::CaretLeft, "L1 moves the caret left"); + check(pad.Press(PadButtonR1).edit == PadEdit::CaretRight, "R1 moves the caret right"); + pad.Step(0); + check(count(pad.Hold(PadButtonR1, 0.5), PadEdit::CaretRight) >= 4, "R1 repeats"); + pad.state.cursorRow = 5; + pad.state.cursorColumn = 1; + pad.Step(0); + check(pad.Press(PadButtonCross).edit == PadEdit::CaretHome, "the up-arrow key moves the caret to the start"); + pad.state.cursorColumn = 0; + check(pad.Press(PadButtonCross).edit == PadEdit::CaretEnd, "the down-arrow key moves the caret to the end"); + Driver multiline({true, true}); + multiline.Step(0); + multiline.state.cursorRow = 5; + multiline.state.cursorColumn = 1; + check(multiline.Press(PadButtonCross).edit == PadEdit::None, "the up-arrow key does nothing in multiline text"); +} + +void clearAll() { + Driver l1First; + l1First.Step(0); + check(l1First.Step(PadButtonL1).edit == PadEdit::CaretLeft, "L1 alone moves the caret on its own edge"); + check(l1First.Step(PadButtonL1 | PadButtonSquare).edit == PadEdit::Clear, "L1 then Square clears"); + Driver squareFirst; + squareFirst.Step(0); + check(squareFirst.Step(PadButtonSquare).edit == PadEdit::Backspace, "Square alone deletes on its own edge"); + check(squareFirst.Step(PadButtonSquare | PadButtonL1).edit == PadEdit::Clear, "Square then L1 clears"); +} + +void shiftCycle() { + Driver pad; + pad.Step(0); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0); + check(pad.state.caseState == PadKeyboardCase::Upper, "L2 shifts once"); + const auto typed = pad.Press(PadButtonCross); + check(typed.text == u"G", "a shifted key types upper case"); + PadKeyboardNoteInsert(pad.state, true); + check(pad.state.caseState == PadKeyboardCase::Lower, "one-shot upper case ends after one typed character"); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0); + check(pad.state.caseState == PadKeyboardCase::CapsLock, "a second L2 locks upper case"); + pad.Press(PadButtonCross); + PadKeyboardNoteInsert(pad.state, true); + check(pad.state.caseState == PadKeyboardCase::CapsLock, "caps lock survives typing"); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0); + check(pad.state.caseState == PadKeyboardCase::Lower, "a third L2 returns to lower case"); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0, 128, 128, 120); + pad.Step(PadButtonL2, 128, 128, 255); + check(pad.state.caseState == PadKeyboardCase::Upper, "L2 does not shift again until the trigger is released past 0.20"); + pad.Step(0, 128, 128, 0); + pad.Step(PadButtonL2, 128, 128, 255); + check(pad.state.caseState == PadKeyboardCase::CapsLock, "a released trigger shifts again"); + Driver notTyped; + notTyped.Step(0); + notTyped.Step(PadButtonL2, 128, 128, 255); + notTyped.Step(0); + notTyped.Press(PadButtonCross); + PadKeyboardNoteInsert(notTyped.state, false); + check(notTyped.state.caseState == PadKeyboardCase::Upper, "a character that did not fit keeps the one-shot shift"); +} + +void symbols() { + Driver l2First; + l2First.Step(0); + l2First.Step(PadButtonL2, 128, 128, 255); + l2First.Step(PadButtonL2 | PadButtonTriangle, 128, 128, 255); + check(l2First.state.family == PadKeyboardFamily::Symbols && l2First.FirstLabel() == u"!", "L2 then Triangle opens the symbols"); + l2First.Step(0); + check(l2First.Press(PadButtonL2).edit == PadEdit::None && l2First.state.family == PadKeyboardFamily::Symbols, "L2 alone does nothing on the symbols"); + l2First.Step(PadButtonL2, 128, 128, 255); + l2First.Step(PadButtonL2 | PadButtonTriangle, 128, 128, 255); + check(l2First.state.family == PadKeyboardFamily::Latin, "L2 and Triangle return to the letters"); + Driver triangleFirst; + triangleFirst.Step(0); + check(triangleFirst.Step(PadButtonTriangle).text == u" ", "Triangle alone types a space"); + const auto toggled = triangleFirst.Step(PadButtonTriangle | PadButtonL2, 128, 128, 255); + check(toggled.edit == PadEdit::None && triangleFirst.state.family == PadKeyboardFamily::Symbols, "Triangle then L2 opens the symbols"); + Driver key; + key.Step(0); + key.state.cursorRow = 4; + key.state.cursorColumn = 1; + check(key.SelectedLabel() == u"@#:", "the symbols key is selected"); + key.Press(PadButtonCross); + check(key.state.family == PadKeyboardFamily::Symbols && key.SelectedLabel() == u"ABC", "the symbols key opens the symbols and keeps the selection on its key"); + key.state.cursorRow = 2; + key.state.cursorColumn = 9; + key.Press(PadButtonCross); + check(key.FirstLabel() == u"‚", "the next-page key shows the second symbols page"); + key.Press(PadButtonCross); + check(key.FirstLabel() == u"!", "the previous-page key returns to the first symbols page"); + key.state.cursorRow = 3; + key.state.cursorColumn = 5; + check(key.SelectedLabel() == u"€", "the currency key follows the Euro default"); + Driver dollar({true, false, PadKeyboardFamily::Symbols, PadKeyboardCase::Lower, PadKeyboardCurrencyFor(0x4), 0}); + dollar.state.cursorRow = 3; + dollar.state.cursorColumn = 5; + check(dollar.SelectedLabel() == u"$", "US English puts the dollar in the currency key"); + check(PadKeyboardCurrencyFor(0x4000 | 0x4) == PadKeyboardCurrency::Won && PadKeyboardCurrencyFor(0x2000) == PadKeyboardCurrency::Yen && PadKeyboardCurrencyFor(0x40000) == PadKeyboardCurrency::Pound, "the currency follows the supported languages"); +} + +void specials() { + Driver pad; + pad.Step(0); + pad.Press(PadButtonL3); + check(pad.state.family == PadKeyboardFamily::Specials && pad.FirstLabel() == u"à", "L3 opens the accented letters"); + pad.Step(PadButtonL2, 128, 128, 255); + pad.Step(0); + check(pad.FirstLabel() == u"À", "L2 shifts the accented letters"); + pad.Press(PadButtonL3); + check(pad.state.family == PadKeyboardFamily::Latin, "L3 returns to the letters"); + pad.Step(PadButtonL2 | PadButtonTriangle, 128, 128, 255); + pad.Step(0); + check(pad.state.family == PadKeyboardFamily::Symbols, "the symbols are open"); + pad.Press(PadButtonL3); + check(pad.state.family == PadKeyboardFamily::Symbols, "L3 does nothing on the symbols"); +} + +void navigation() { + Driver pad; + pad.Step(0); + pad.Press(PadButtonRight); + check(pad.SelectedLabel() == u"h", "right moves one key"); + pad.Press(PadButtonDown); + check(pad.SelectedLabel() == u"n", "down moves one row"); + pad.state.cursorRow = 2; + pad.state.cursorColumn = 0; + pad.Press(PadButtonLeft); + check(pad.SelectedLabel() == u"'", "a single press wraps to the far edge at once"); + pad.state.cursorRow = 2; + pad.state.cursorColumn = 1; + pad.Step(0); + pad.Hold(PadButtonLeft, 0.4); + check(pad.SelectedLabel() == u"a", "a held direction stops at the edge before wrapping"); + pad.Hold(PadButtonLeft, 0.6); + check(pad.SelectedLabel() != u"a", "the held direction wraps after half a second"); + pad.Step(0); + pad.state.cursorRow = 4; + pad.state.cursorColumn = 3; + pad.Press(PadButtonRight); + check(pad.SelectedLabel() == u"" && pad.state.cursorColumn == 8, "a spanned key is one step and empty cells are skipped"); + pad.state.cursorRow = 0; + pad.state.cursorColumn = 2; + pad.Press(PadButtonUp); + check(pad.state.cursorRow == 5 && pad.state.cursorColumn == 2, "up from the top row wraps to the bottom row"); + Driver stick; + stick.Step(0); + stick.Step(0, 150, 128); + check(stick.SelectedLabel() == u"g", "a stick inside the dead zone does not move"); + stick.Step(0, 255, 128); + check(stick.SelectedLabel() == u"h", "the stick moves once past the threshold"); + for (int i = 0; i < 20; ++i) stick.Step(0, 255, 128); + check(stick.SelectedLabel() != u"h", "a held stick repeats"); +} + +void repeatsOnlyEditingKeys() { + Driver typed; + typed.Step(0); + const auto characters = count(typed.Hold(PadButtonCross, 0.5), PadEdit::Insert); + check(characters >= 4, "Cross repeats a character key: " + std::to_string(characters)); + Driver shift; + shift.Step(0); + shift.state.cursorRow = 4; + shift.state.cursorColumn = 0; + shift.Hold(PadButtonCross, 0.5); + check(shift.state.caseState == PadKeyboardCase::Upper, "Cross does not repeat the shift key"); +} + +void priority() { + Driver pad; + pad.Step(0); + const auto both = pad.Step(PadButtonTriangle | PadButtonR2); + check(both.edit == PadEdit::Insert && both.finish == PadFinish::None, "the space outranks Done in one step"); + pad.Step(0); + pad.state.cursorRow = 2; + pad.state.cursorColumn = 4; + const auto grid = pad.Step(PadButtonCross | PadButtonSquare); + check(grid.edit == PadEdit::Insert && grid.text == u"g", "a key the grid activates outranks the shortcuts"); +} + +void controllerDisabled() { + Driver pad({false}); + pad.Step(0); + for (const std::uint32_t button : {PadButtonCross, PadButtonR2, PadButtonCircle, PadButtonSquare, PadButtonRight}) { + const auto result = pad.Press(button); + check(result.edit == PadEdit::None && result.finish == PadFinish::None, "a disabled controller drives nothing"); + } + check(pad.SelectedLabel() == u"g", "a disabled controller does not move the selection"); +} + +void initialPages() { + Driver symbols({true, false, PadKeyboardFamily::Symbols}); + check(symbols.SelectedLabel() == u"!", "the symbols open on their first key"); + Driver locked({true, false, PadKeyboardFamily::Latin, PadKeyboardCase::CapsLock}); + check(locked.SelectedLabel() == u"1" && locked.state.caseState == PadKeyboardCase::CapsLock, "a shift-locked start uses the first key"); +} + +} + +int main() { + const std::vector>> tests{ + {"starts on g", startsOnG}, + {"held at open", heldAtOpenDoesNothing}, + {"finishes", finishes}, + {"space and backspace", spaceAndBackspace}, + {"caret", caret}, + {"clear all", clearAll}, + {"shift cycle", shiftCycle}, + {"symbols", symbols}, + {"specials", specials}, + {"navigation", navigation}, + {"repeats only editing keys", repeatsOnlyEditingKeys}, + {"priority", priority}, + {"controller disabled", controllerDisabled}, + {"initial pages", initialPages}, + }; + int failed = 0; + for (const auto& [name, test] : tests) { + try { + test(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL %s: %s\n", name, error.what()); + ++failed; + } + } + if (failed != 0) return 1; + std::printf("pad keyboard tests passed\n"); + return 0; +}