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.
This commit is contained in:
Yunus Shaikh authored and Adrià Franch committed 2026-10-27 05:19:40 +01:00
1 parent b986c11a34
commit d1d104d67b
4 files changed
+1356 -1

No files matched your search

+9 -1
View File
@@ -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})
@@ -0,0 +1,129 @@
#ifndef CORE_LIBS_PRX_LIBSCEIMEDIALOG_INCLUDE_PADKEYBOARD_HPP
#define CORE_LIBS_PRX_LIBSCEIMEDIALOG_INCLUDE_PADKEYBOARD_HPP
#include <array>
#include <chrono>
#include <cstdint>
#include <span>
#include <string>
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<bool, 4> 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<const PadKey> 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
@@ -0,0 +1,860 @@
#include "prx/libSceImeDialog/include/PadKeyboard.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
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<const PadKey> 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<int, int> 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<int>::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<int, int> nearest(const Layout& layout, int row, int column) {
std::pair<int, int> best{-1, -1};
int bestDistance = std::numeric_limits<int>::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<int, int> 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<double>(value) - 128.0) / 127.0, -1.0, 1.0);
return std::abs(centred) < PadStickDeadzone ? 0.0 : centred;
}
std::array<bool, 4> 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<bool, 4>& 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<std::size_t>(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<std::uint8_t>((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<double>(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<bool, 4> 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<double>(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<const PadKey> 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;
}
}
@@ -0,0 +1,358 @@
#include "prx/libSceImeDialog/include/PadKeyboard.hpp"
#include <chrono>
#include <cstdio>
#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
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<Clock::duration>(std::chrono::duration<double>(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<PadKeyboardResult> Hold(std::uint32_t buttons, double seconds) {
std::vector<PadKeyboardResult> 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<PadKeyboardResult>& 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<std::pair<const char*, std::function<void()>>> 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;
}