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; +}