#include "SceTypes.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceVideoOut/include/ImeTextInput.hpp" #include #include #include #include #include #include #include extern "C" { int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended); int APS5_VABI sceImeClose_nid_postfix(void); int APS5_VABI sceImeUpdate(EventHandler handler); int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); int APS5_VABI sceImeSetCaret(const Caret* caret); int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form); void APS5_VABI sceImeParamInit(Param* param); } namespace { constexpr int Busy = static_cast(0x80bc0001u); constexpr int NotOpened = static_cast(0x80bc0002u); constexpr int InvalidText = static_cast(0x80bc0006u); constexpr int InvalidUserId = static_cast(0x80bc0010u); constexpr int InvalidType = static_cast(0x80bc0011u); constexpr int InvalidEnterLabel = static_cast(0x80bc0013u); constexpr int InvalidInputMethod = static_cast(0x80bc0014u); constexpr int InvalidOption = static_cast(0x80bc0015u); constexpr int InvalidMaxTextLength = static_cast(0x80bc0016u); constexpr int InvalidInputTextBuffer = static_cast(0x80bc0017u); constexpr int InvalidPosX = static_cast(0x80bc0018u); constexpr int InvalidPosY = static_cast(0x80bc0019u); constexpr int InvalidHorizontalAlignment = static_cast(0x80bc001au); constexpr int InvalidVerticalAlignment = static_cast(0x80bc001bu); constexpr int InvalidExtended = static_cast(0x80bc001cu); constexpr int InvalidWork = static_cast(0x80bc0020u); constexpr int InvalidHandler = static_cast(0x80bc0022u); constexpr int InvalidParam = static_cast(0x80bc0030u); constexpr int InvalidAddress = static_cast(0x80bc0031u); constexpr int InvalidReserved = static_cast(0x80bc0032u); constexpr uint32_t EventOpen = 0; constexpr uint32_t EventUpdateText = 1; constexpr uint32_t EventUpdateCaret = 2; constexpr uint32_t EventPressClose = 4; constexpr uint32_t EventPressEnter = 5; struct Recorded { uint32_t id; void* arg; ImeEventParam param; std::u16string text; }; std::vector g_events; bool g_closeOnPressClose = false; int g_reentrantClose = 1; void APS5_VABI Record(void* arg, const ImeEvent* event) { Recorded recorded{event->id, arg, event->param, {}}; if (event->id == EventUpdateText || event->id == EventPressEnter || event->id == EventPressClose) { recorded.text = event->param.text.str; } g_events.push_back(recorded); if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); } void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); std::abort(); } struct Buffers { char16_t text[2049] = {}; alignas(16) unsigned char work[0x5000] = {}; int arg = 0; }; Param MakeParam(Buffers& buffers, uint32_t maxLength) { Param param; sceImeParamInit(¶m); param.user_id = 1; param.max_text_length = maxLength; param.input_text_buffer = buffers.text; param.posx = 100.0f; param.posy = 200.0f; param.work = buffers.work; param.arg = &buffers.arg; param.handler = Record; return param; } std::vector Drain() { g_events.clear(); Require(sceImeUpdate(Record) == 0, "update failed"); return g_events; } void Publish(ImeHostInputKind kind, const char* text = "") { ImeHostInput input{}; input.kind = kind; std::strncpy(input.text, text, sizeof(input.text) - 1); ImePublishInput_nid_postfix(input); } void RequireText(const Recorded& event, uint32_t id, const std::u16string& text, uint32_t caret, uint32_t index, int32_t delta) { Require(event.id == id, "event id"); Require(event.text == text, "event text"); Require(event.param.text.caret_index == caret, "event caret"); Require(event.param.text.area_num == 1, "event area count"); Require(event.param.text.text_area[0].mode == TextAreaMode::Edit, "event area mode"); Require(event.param.text.text_area[0].index == index, "event edit index"); Require(event.param.text.text_area[0].length == delta, "event edit length"); } void RequireCaretSteps(const std::vector& events, uint32_t direction, std::size_t count) { Require(events.size() == count, "caret step count"); for (const auto& event : events) { Require(event.id == EventUpdateCaret && event.param.caret_move == direction, "caret step"); } } void CheckValidation() { Buffers buffers; Require(sceImeOpen_nid_postfix(nullptr, nullptr) == InvalidAddress, "null param"); auto expect = [&](auto change, int error, const char* message) { Param param = MakeParam(buffers, 16); change(param); Require(sceImeOpen_nid_postfix(¶m, nullptr) == error, message); }; expect([](Param& p) { p.user_id = -1; }, InvalidUserId, "invalid user"); expect([](Param& p) { p.type = 5; }, InvalidType, "invalid type"); expect([](Param& p) { p.enter_label = 4; }, InvalidEnterLabel, "invalid enter label"); expect([](Param& p) { p.input_method = 1; }, InvalidInputMethod, "invalid input method"); expect([](Param& p) { p.option = 0x80000000u; }, InvalidOption, "invalid option"); expect([](Param& p) { p.max_text_length = 0; }, InvalidMaxTextLength, "zero length"); expect([](Param& p) { p.max_text_length = 2049; }, InvalidMaxTextLength, "long length"); expect([](Param& p) { p.input_text_buffer = nullptr; }, InvalidInputTextBuffer, "null buffer"); expect([](Param& p) { p.posx = -1.0f; }, InvalidPosX, "negative x"); expect([](Param& p) { p.posx = 1920.0f; }, InvalidPosX, "x past 1080p"); expect([](Param& p) { p.posy = 1080.0f; }, InvalidPosY, "y past 1080p"); expect([](Param& p) { p.horizontal_alignment = 3; }, InvalidHorizontalAlignment, "horizontal alignment"); expect([](Param& p) { p.vertical_alignment = 3; }, InvalidVerticalAlignment, "vertical alignment"); expect([](Param& p) { p.work = nullptr; }, InvalidWork, "null work"); expect([&](Param& p) { p.work = buffers.work + 2; }, InvalidWork, "misaligned work"); expect([](Param& p) { p.handler = nullptr; }, InvalidHandler, "null handler"); expect([](Param& p) { p.reserved[7] = 1; }, InvalidReserved, "reserved bytes"); buffers.text[0] = 0xdc00; expect([](Param&) {}, InvalidText, "lone low surrogate"); buffers.text[0] = u'\n'; expect([](Param&) {}, InvalidText, "newline without multiline"); buffers.text[0] = 0; Param param = MakeParam(buffers, 16); ExtendedParam extended{}; extended.priority = 4; Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended priority"); extended.priority = 0; extended.reserved[59] = 1; Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended reserved"); param.posx = 3000.0f; param.option = 0x4000; Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "over-2K coordinates"); Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); param = MakeParam(buffers, 16); param.filter = reinterpret_cast(&Record); bool threw = false; try { sceImeOpen_nid_postfix(¶m, nullptr); } catch (const std::exception&) { threw = true; } Require(threw, "text filter must throw"); Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); } void CheckEditing() { Buffers buffers; std::memcpy(buffers.text, u"ab", sizeof(u"ab")); Param param = MakeParam(buffers, 8); Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured before open"); Require(sceImeUpdate(Record) == 0 && g_events.empty(), "update before open"); Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); Require(sceImeOpen_nid_postfix(¶m, nullptr) == Busy, "second open"); Require(ImeCapturesKeyboard_nid_postfix(), "keyboard not captured"); Require(sceImeUpdate(Other) == 0, "update with another handler"); auto events = Drain(); Require(events.size() == 1 && events[0].id == EventOpen, "open event"); Require(events[0].arg == &buffers.arg, "handler argument"); Require(events[0].param.rect.x == 100.0f && events[0].param.rect.y == 200.0f, "open position"); Require(events[0].param.rect.width == 793 && events[0].param.rect.height == 408, "open size"); Require(Drain().empty(), "events delivered twice"); PositionAndForm form{}; Require(sceImeGetPanelPositionAndForm(nullptr) == InvalidAddress, "null panel form"); Require(sceImeGetPanelPositionAndForm(&form) == 0, "panel form"); Require(form.posx == 100.0f && form.posy == 200.0f && form.width == 793 && form.height == 408, "panel form values"); Publish(ImeHostInputKind::Text, "c"); events = Drain(); Require(events.size() == 1, "one text event"); RequireText(events[0], EventUpdateText, u"abc", 3, 2, 1); Require(std::u16string(buffers.text) == u"abc", "input buffer"); Require(std::u16string(reinterpret_cast(buffers.work)) == u"abc", "work copy"); Publish(ImeHostInputKind::Text, "\xc3\xa9\xf0\x9f\x98\x80"); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abcĂ©\U0001F600", 6, 3, 3); Publish(ImeHostInputKind::Left); RequireCaretSteps(Drain(), 1, 2); Publish(ImeHostInputKind::Backspace); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abc\U0001F600", 3, 3, -1); Publish(ImeHostInputKind::Delete); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abc", 3, 3, -2); Publish(ImeHostInputKind::Delete); Require(Drain().empty(), "delete at the end"); Publish(ImeHostInputKind::Home); RequireCaretSteps(Drain(), 1, 3); Publish(ImeHostInputKind::Left); Require(Drain().empty(), "left at the start"); Publish(ImeHostInputKind::Backspace); Require(Drain().empty(), "backspace at the start"); Publish(ImeHostInputKind::Right); RequireCaretSteps(Drain(), 2, 1); Publish(ImeHostInputKind::End); RequireCaretSteps(Drain(), 2, 2); Publish(ImeHostInputKind::Text, "x\ny"); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abcxy", 5, 3, 2); Publish(ImeHostInputKind::Text, "12\xf0\x9f\x98\x80"); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abcxy12", 7, 5, 2); Publish(ImeHostInputKind::Text, "\xf0\x9f\x98\x80"); Require(Drain().empty(), "a pair past the limit must not be split"); Publish(ImeHostInputKind::Text, "z"); events = Drain(); RequireText(events.at(0), EventUpdateText, u"abcxy12z", 8, 7, 1); Publish(ImeHostInputKind::Text, "q"); Require(Drain().empty(), "text past the limit"); Require(sceImeSetText(nullptr, 1) == InvalidAddress, "null text"); Require(sceImeSetText(u"a\nb", 3) == InvalidText, "newline in single-line text"); Require(sceImeSetText(u"hello", 5) == 0, "set text"); Require(std::u16string(buffers.text) == u"hello", "set text buffer"); Require(Drain().empty(), "set text queued an event"); Caret caret{}; caret.index = 6; Require(sceImeSetCaret(nullptr) == InvalidAddress, "null caret"); Require(sceImeSetCaret(&caret) == InvalidParam, "caret past the text"); caret.index = 1; Require(sceImeSetCaret(&caret) == 0, "set caret"); Publish(ImeHostInputKind::Text, "X"); events = Drain(); RequireText(events.at(0), EventUpdateText, u"hXello", 2, 1, 1); Publish(ImeHostInputKind::Enter); events = Drain(); RequireText(events.at(0), EventPressEnter, u"hXello", 2, 2, 0); g_closeOnPressClose = true; Publish(ImeHostInputKind::Escape); Publish(ImeHostInputKind::Enter); events = Drain(); g_closeOnPressClose = false; Require(events.size() == 1, "events after a close inside the handler"); RequireText(events[0], EventPressClose, u"hXello", 2, 2, 0); Require(g_reentrantClose == 0, "close inside the handler"); Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured after close"); Require(sceImeClose_nid_postfix() == NotOpened, "second close"); Publish(ImeHostInputKind::Text, "ignored"); Require(std::u16string(buffers.text) == u"hXello", "input after close"); Require(sceImeSetText(u"a", 1) == NotOpened, "set text after close"); } void CheckMultiline() { Buffers buffers; Param param = MakeParam(buffers, 16); param.option = 1; Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "multiline open"); Drain(); Publish(ImeHostInputKind::Text, "a"); Publish(ImeHostInputKind::Enter); Publish(ImeHostInputKind::Text, "b\nc"); const auto events = Drain(); Require(events.size() == 3, "multiline events"); RequireText(events[1], EventUpdateText, u"a\nb\nc", 2, 1, 1); RequireText(events[2], EventUpdateText, u"a\nb\nc", 5, 2, 3); Require(sceImeSetText(u"x\ny", 3) == 0, "multiline set text"); Require(sceImeClose_nid_postfix() == 0, "multiline close"); } void CheckHostEvents() { Buffers buffers; Param param = MakeParam(buffers, 16); Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "host events open"); Drain(); ImeTextInput input; SDL_Event event{}; event.type = SDL_TEXTINPUT; event.text.windowID = 7; std::strcpy(event.text.text, "hi"); input.HandleEvent(event, 7); event.text.windowID = 8; input.HandleEvent(event, 7); event = {}; event.type = SDL_KEYDOWN; event.key.windowID = 7; event.key.keysym.scancode = SDL_SCANCODE_BACKSPACE; event.key.repeat = 1; input.HandleEvent(event, 7); event.key.keysym.scancode = SDL_SCANCODE_RETURN; input.HandleEvent(event, 7); event.key.repeat = 0; input.HandleEvent(event, 7); event.key.keysym.scancode = SDL_SCANCODE_A; input.HandleEvent(event, 7); event.type = SDL_KEYUP; event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; input.HandleEvent(event, 7); const auto events = Drain(); Require(events.size() == 3, "host event count"); RequireText(events[0], EventUpdateText, u"h", 2, 0, 2); RequireText(events[1], EventUpdateText, u"h", 1, 1, -1); Require(events[2].id == EventPressEnter, "host enter"); Require(sceImeClose_nid_postfix() == 0, "host events close"); } } int main() { CheckValidation(); CheckEditing(); CheckMultiline(); CheckHostEvents(); }