diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f81661..bb6991a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,12 +105,12 @@ jobs: - name: Build shell: cmd run: | - cmake --build _Build/windows --target launcher audio_out2_port_tests virtual_memory_allocation_tests --parallel + cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel - name: Test shell: cmd run: | - ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|virtual_memory_allocation)$" + ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$" - name: Install shell: cmd @@ -186,14 +186,14 @@ jobs: shell: bash run: | cmake --build _Build/macos \ - --target launcher audio_out2_port_tests virtual_memory_allocation_tests \ + --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \ --parallel - name: Test shell: bash run: | ctest --test-dir _Build/macos --output-on-failure \ - -R '^(audio_out2_port|virtual_memory_allocation)$' + -R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$' - name: Install shell: bash @@ -329,14 +329,14 @@ jobs: run: | cmake --build _Build/linux \ --target launcher page_manager_tests memory_tracker_tests \ - audio_out2_port_tests virtual_memory_allocation_tests \ + audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \ --parallel - name: Test shell: bash run: | ctest --test-dir _Build/linux --output-on-failure \ - -R '^(audio_out2_port|page_manager|memory_tracker|virtual_memory_allocation)$' + -R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$' - name: Install shell: bash diff --git a/.gitmodules b/.gitmodules index ea1f6ac..960ba8a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -39,3 +39,6 @@ [submodule "3rdparty/nlohmann_json"] path = 3rdparty/nlohmann_json url = https://github.com/nlohmann/json.git +[submodule "3rdparty/imgui"] + path = 3rdparty/imgui + url = https://github.com/ocornut/imgui.git diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 13daa9c..c0d59cd 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -11,6 +11,25 @@ target_include_directories(Vulkan-Headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/Vulkan-Headers/include" ) +add_library(imgui STATIC + imgui/imgui.cpp + imgui/imgui_draw.cpp + imgui/imgui_tables.cpp + imgui/imgui_widgets.cpp + imgui/backends/imgui_impl_vulkan.cpp +) +target_include_directories(imgui PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/imgui" + "${CMAKE_CURRENT_SOURCE_DIR}/imgui/backends" +) +target_compile_definitions(imgui PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES) +target_link_libraries(imgui PUBLIC Vulkan::Headers) +if(MSVC) + target_compile_options(imgui PRIVATE /W0) +else() + target_compile_options(imgui PRIVATE -w) +endif() + set(SPIRV_SKIP_TESTS ON CACHE BOOL "" FORCE) set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "" FORCE) set(SKIP_SPIRV_TOOLS_INSTALL ON CACHE BOOL "" FORCE) diff --git a/3rdparty/imgui b/3rdparty/imgui new file mode 160000 index 0000000..f1cc2ae --- /dev/null +++ b/3rdparty/imgui @@ -0,0 +1 @@ +Subproject commit f1cc2ae15e53a861a874c3034aae6798fde194ab diff --git a/CMakeLists.txt b/CMakeLists.txt index 36c6901..a031486 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,7 +292,7 @@ if (KYTY_CLANG_CL) set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED") endif() -set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version) +set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version) # Linux system libraries required by the static FFmpeg archive. if(LINUX) @@ -385,6 +385,12 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL ) target_include_directories(bit_array_tests PRIVATE ${inc_headers}) +add_executable(ime_dialog_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/ImeDialogTests.cpp" + "${KYTY_SOURCE_DIR}/libs/imeDialog.cpp" +) +target_include_directories(ime_dialog_tests PRIVATE ${inc_headers}) + add_executable(memory_tracker_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/MemoryTrackerTests.cpp" "${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp" @@ -499,6 +505,7 @@ if(NOT KYTY_CLANG_CL) endif() if(BUILD_TESTING) + add_test(NAME ime_dialog COMMAND $) add_test(NAME shader_cfg COMMAND $) add_test(NAME scalar_provenance COMMAND $) add_test(NAME image_page_table COMMAND $) @@ -548,6 +555,7 @@ if(BUILD_TESTING) endif() add_custom_target(kyty_tests DEPENDS + ime_dialog_tests shader_cfg_tests scalar_provenance_tests image_page_table_tests diff --git a/src/graphics/presentation/imeDialogOverlay.cpp b/src/graphics/presentation/imeDialogOverlay.cpp new file mode 100644 index 0000000..e6f405a --- /dev/null +++ b/src/graphics/presentation/imeDialogOverlay.cpp @@ -0,0 +1,876 @@ +#include "graphics/presentation/imeDialogOverlay.h" + +#include "SDL.h" +#include "common/assert.h" +#include "common/stringUtils.h" +#include "graphics/host_gpu/graphicContext.h" +#include "imgui.h" +#include "imgui_impl_vulkan.h" +#include "libs/controller.h" +#include "libs/imeDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Graphics { + +namespace { + +namespace Ime = Libs::Dialog::ImeDialog; + +constexpr size_t INPUT_QUEUE_CAPACITY = 128; + +enum class InputKind : uint8_t { + Button, + Axis, + MousePosition, + MouseButton, + MouseWheel, + ResetController +}; + +struct InputEvent { + InputKind kind; + uint64_t generation; + int id; + float x; + float y; +}; + +struct VisibilityUpdate { + uint64_t generation; + bool visible; + bool capture_controller; + bool capture_keyboard; + bool multiline; +}; + +std::atomic g_visibility_event {static_cast(-1)}; +std::mutex g_visibility_mutex; +std::mutex g_input_mutex; +std::deque g_input_events; +std::deque g_visibility_updates; +size_t g_missing_visibility_wakeups = 0; +bool g_input_reset_requested = false; +uint16_t g_last_external_keycode = 0; +uint32_t g_last_external_status = 0; +uint64_t g_input_generation = 0; +bool g_input_active = false; +bool g_input_controller = false; +bool g_input_keyboard = false; +bool g_input_multiline = false; +bool g_input_lifecycle_active = false; +bool g_active = false; +bool g_controller_captured = false; +uint64_t g_generation = 0; + +void ClearInputEvents() { + std::scoped_lock lock(g_input_mutex); + g_input_events.clear(); + g_input_reset_requested = false; +} + +void QueueInput(InputEvent event) { + std::scoped_lock lock(g_input_mutex); + const bool replaceable = + event.kind == InputKind::Axis || event.kind == InputKind::MousePosition; + if (replaceable && !g_input_events.empty()) { + auto& last = g_input_events.back(); + if (last.generation == event.generation && last.kind == event.kind && last.id == event.id) { + last = event; + return; + } + } + if (g_input_events.size() == INPUT_QUEUE_CAPACITY) { + g_input_events.clear(); + g_input_reset_requested = true; + } + g_input_events.push_back(event); +} + +void RetryVisibilityWakeup() { + const Uint32 type = g_visibility_event.load(std::memory_order_acquire); + if (type == static_cast(-1)) { + return; + } + std::scoped_lock lock(g_input_mutex); + if (g_missing_visibility_wakeups == 0) { + return; + } + SDL_Event event {}; + event.type = type; + if (SDL_PushEvent(&event) > 0) { + g_missing_visibility_wakeups--; + } +} + +void OnVisibilityChanged(bool visible, uint64_t generation) { + std::scoped_lock visibility_lock(g_visibility_mutex); + if (!g_input_lifecycle_active) { + return; + } + if (generation < g_generation || (generation == g_generation && visible == g_active)) { + return; + } + + g_generation = generation; + bool capture_controller = false; + bool capture_keyboard = false; + bool multiline = false; + if (visible) { + Ime::HostSnapshot snapshot; + if (Ime::GetHostSnapshot(&snapshot) && snapshot.generation == generation) { + capture_controller = (snapshot.disable_device & Ime::DISABLE_DEVICE_CONTROLLER) == 0; + capture_keyboard = (snapshot.disable_device & Ime::DISABLE_DEVICE_EXT_KEYBOARD) == 0; + multiline = (snapshot.option & Ime::OPTION_MULTILINE) != 0; + } else { + visible = false; + } + } + const bool was_controller = std::exchange(g_controller_captured, capture_controller); + if (capture_controller || was_controller) { + Controller::ControllerResetInputState(); + } + g_active = visible; + + const Uint32 type = g_visibility_event.load(std::memory_order_acquire); + if (type != static_cast(-1)) { + SDL_Event event {}; + event.type = type; + std::scoped_lock input_lock(g_input_mutex); + g_visibility_updates.push_back( + {generation, visible, capture_controller, capture_keyboard, multiline}); + if (g_missing_visibility_wakeups != 0 || SDL_PushEvent(&event) <= 0) { + g_missing_visibility_wakeups++; + } + } +} + +uint32_t ExternalKeyStatus(SDL_Keymod modifiers, bool character_valid) { + uint32_t status = 0x00000001 | (character_valid ? 0x00000002 : 0); + if ((modifiers & KMOD_LCTRL) != 0) status |= 0x00000100; + if ((modifiers & KMOD_LSHIFT) != 0) status |= 0x00000200; + if ((modifiers & KMOD_LALT) != 0) status |= 0x00000400; + if ((modifiers & KMOD_LGUI) != 0) status |= 0x00000800; + if ((modifiers & KMOD_RCTRL) != 0) status |= 0x00001000; + if ((modifiers & KMOD_RSHIFT) != 0) status |= 0x00002000; + if ((modifiers & KMOD_RALT) != 0) status |= 0x00004000; + if ((modifiers & KMOD_RGUI) != 0) status |= 0x00008000; + if ((modifiers & KMOD_NUM) != 0) status |= 0x00010000; + if ((modifiers & KMOD_CAPS) != 0) status |= 0x00020000; + return status; +} + +Ime::ExternalInput MakeExternalInput(Ime::ExternalAction action, uint16_t keycode, + uint32_t status) { + Ime::ExternalInput input {}; + input.key.keycode = keycode; + input.key.status = status; + input.key.type = 4; + input.action = action; + return input; +} + +std::u16string Utf8ToUtf16(std::string_view text) { + std::u16string result; + result.reserve(text.size()); + for (size_t i = 0; i < text.size();) { + const auto first = static_cast(text[i++]); + uint32_t codepoint = 0; + uint32_t remaining = 0; + if (first < 0x80) { + codepoint = first; + } else if ((first & 0xe0) == 0xc0) { + codepoint = first & 0x1f; + remaining = 1; + } else if ((first & 0xf0) == 0xe0) { + codepoint = first & 0x0f; + remaining = 2; + } else if ((first & 0xf8) == 0xf0) { + codepoint = first & 0x07; + remaining = 3; + } else { + continue; + } + if (i + remaining > text.size()) { + break; + } + bool valid = true; + for (uint32_t j = 0; j < remaining; j++) { + const auto next = static_cast(text[i++]); + if ((next & 0xc0) != 0x80) { + valid = false; + break; + } + codepoint = (codepoint << 6) | (next & 0x3f); + } + if (!valid || codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)) { + continue; + } + if (codepoint <= 0xffff) { + result.push_back(static_cast(codepoint)); + } else { + codepoint -= 0x10000; + result.push_back(static_cast(0xd800 + (codepoint >> 10))); + result.push_back(static_cast(0xdc00 + (codepoint & 0x3ff))); + } + } + return result; +} + +std::string VisibleText(const Ime::HostSnapshot& snapshot, size_t max_units) { + const size_t cursor = std::min(snapshot.cursor, snapshot.text.size()); + const size_t payload_units = max_units > 4 ? max_units - 4 : 1; + size_t begin = cursor > payload_units / 2 ? cursor - payload_units / 2 : 0; + size_t end = std::min(snapshot.text.size(), begin + payload_units); + if (end == snapshot.text.size() && end - begin < payload_units) { + begin = end > payload_units ? end - payload_units : 0; + } + if (begin > 0 && snapshot.text[begin] >= 0xdc00 && snapshot.text[begin] <= 0xdfff) { + begin--; + } + if (end < snapshot.text.size() && end > begin && snapshot.text[end - 1] >= 0xd800 && + snapshot.text[end - 1] <= 0xdbff) { + end--; + } + + std::u16string visible; + if (begin != 0) { + visible.push_back(u'\u2026'); + } + const size_t caret = visible.size() + cursor - begin; + if ((snapshot.option & Ime::OPTION_PASSWORD) != 0) { + visible.append(end - begin, u'*'); + } else { + visible.append(snapshot.text, begin, end - begin); + std::replace(visible.begin(), visible.end(), u'\n', u'\u21b5'); + std::replace(visible.begin(), visible.end(), u'\r', u'\u21b5'); + } + visible.insert(std::min(caret, visible.size()), 1, u'|'); + if (end != snapshot.text.size()) { + visible.push_back(u'\u2026'); + } + return Common::Utf16ToUtf8(visible.c_str()); +} + +const char* EnterLabel(Ime::EnterLabel label) { + switch (label) { + case Ime::EnterLabel::Send: return "Send"; + case Ime::EnterLabel::Search: return "Search"; + case Ime::EnterLabel::Go: return "Go"; + default: return "Done"; + } +} + +float AlignmentPivot(Ime::Alignment alignment) { + switch (alignment) { + case Ime::Alignment::Start: return 0.0f; + case Ime::Alignment::End: return 1.0f; + default: return 0.5f; + } +} + +ImGuiKey ControllerButtonToKey(int button) { + switch (button) { + case SDL_CONTROLLER_BUTTON_A: return ImGuiKey_GamepadFaceDown; + case SDL_CONTROLLER_BUTTON_B: return ImGuiKey_GamepadFaceRight; + case SDL_CONTROLLER_BUTTON_X: return ImGuiKey_GamepadFaceLeft; + case SDL_CONTROLLER_BUTTON_Y: return ImGuiKey_GamepadFaceUp; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return ImGuiKey_GamepadDpadLeft; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return ImGuiKey_GamepadDpadRight; + case SDL_CONTROLLER_BUTTON_DPAD_UP: return ImGuiKey_GamepadDpadUp; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return ImGuiKey_GamepadDpadDown; + default: return ImGuiKey_None; + } +} + +PFN_vkVoidFunction LoadVulkanFunction(const char* name, void* user_data) { + auto& graphics = *static_cast(user_data); + return graphics.instance.getProcAddr(name); +} + +void CheckVulkanResult(VkResult result) { + EXIT_IF(result != VK_SUCCESS); +} + +} // namespace + +void InitializeImeDialogInput() { + const Uint32 type = SDL_RegisterEvents(1); + EXIT_IF(type == static_cast(-1)); + { + std::scoped_lock lock(g_visibility_mutex); + g_input_lifecycle_active = true; + g_visibility_event.store(type, std::memory_order_release); + } + Ime::SetVisibilityCallback(OnVisibilityChanged); + Ime::HostSnapshot snapshot; + if (Ime::GetHostSnapshot(&snapshot)) { + OnVisibilityChanged(true, snapshot.generation); + } +} + +void ShutdownImeDialogInput() { + Ime::SetVisibilityCallback(nullptr); + { + std::scoped_lock lock(g_visibility_mutex); + g_input_lifecycle_active = false; + g_active = false; + if (std::exchange(g_controller_captured, false)) { + Controller::ControllerResetInputState(); + } + g_visibility_event.store(static_cast(-1), std::memory_order_release); + } + ClearInputEvents(); + { + std::scoped_lock lock(g_input_mutex); + g_visibility_updates.clear(); + g_missing_visibility_wakeups = 0; + } + g_input_active = false; + g_input_controller = false; + g_input_keyboard = false; + g_input_multiline = false; + if (SDL_IsTextInputActive() == SDL_TRUE) { + SDL_StopTextInput(); + } +} + +bool ProcessImeDialogInput(const SDL_Event& event) { + RetryVisibilityWakeup(); + if (event.type == g_visibility_event.load(std::memory_order_acquire)) { + VisibilityUpdate update {}; + { + std::scoped_lock lock(g_input_mutex); + if (g_visibility_updates.empty()) { + return true; + } + update = g_visibility_updates.front(); + g_visibility_updates.pop_front(); + } + ClearInputEvents(); + g_last_external_keycode = 0; + g_last_external_status = 0; + g_input_generation = update.generation; + g_input_active = update.visible; + g_input_controller = update.capture_controller; + g_input_keyboard = update.capture_keyboard; + g_input_multiline = update.multiline; + if (g_input_keyboard) { + SDL_StartTextInput(); + } else if (SDL_IsTextInputActive() == SDL_TRUE) { + SDL_StopTextInput(); + } + return true; + } + if (event.type == SDL_CONTROLLERDEVICEREMOVED) { + if (g_input_active && g_input_controller) { + QueueInput({InputKind::ResetController, g_input_generation, 0, 0.0f, 0.0f}); + } + return false; + } + if (!g_input_active) { + return false; + } + + const uint64_t generation = g_input_generation; + const bool controller_event = event.type == SDL_CONTROLLERBUTTONDOWN || + event.type == SDL_CONTROLLERBUTTONUP || + event.type == SDL_CONTROLLERAXISMOTION; + if (controller_event && !g_input_controller) { + return false; + } + const bool keyboard_event = event.type == SDL_TEXTINPUT || event.type == SDL_TEXTEDITING || + event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; + if (keyboard_event && !g_input_keyboard) { + return false; + } + switch (event.type) { + case SDL_TEXTINPUT: { + const auto text = Utf8ToUtf16(event.text.text); + if (!text.empty()) { + auto input = MakeExternalInput(Ime::ExternalAction::Text, g_last_external_keycode, + g_last_external_status | 0x00000002); + input.key.character = text.front(); + input.text = text; + Ime::HostQueueExternalInput(generation, std::move(input)); + } + return true; + } + case SDL_TEXTEDITING: return true; + case SDL_KEYDOWN: { + g_last_external_keycode = static_cast(event.key.keysym.scancode); + g_last_external_status = + ExternalKeyStatus(static_cast(event.key.keysym.mod), false); + auto action = Ime::ExternalAction::Text; + bool queue = true; + if (event.key.keysym.sym == SDLK_BACKSPACE) { + action = Ime::ExternalAction::Backspace; + } else if (event.key.keysym.sym == SDLK_LEFT) { + action = Ime::ExternalAction::MoveLeft; + } else if (event.key.keysym.sym == SDLK_RIGHT) { + action = Ime::ExternalAction::MoveRight; + } else if (event.key.keysym.sym == SDLK_ESCAPE) { + action = Ime::ExternalAction::Cancel; + } else if (event.key.keysym.sym == SDLK_RETURN || + event.key.keysym.sym == SDLK_KP_ENTER) { + action = + g_input_multiline ? Ime::ExternalAction::Newline : Ime::ExternalAction::Accept; + } else if (event.key.keysym.sym == SDLK_TAB) { + action = Ime::ExternalAction::None; + } else { + queue = false; + } + if (queue) { + Ime::HostQueueExternalInput( + generation, + MakeExternalInput(action, g_last_external_keycode, g_last_external_status)); + } + return true; + } + case SDL_KEYUP: return true; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + QueueInput({InputKind::Button, generation, event.cbutton.button, + event.type == SDL_CONTROLLERBUTTONDOWN ? 1.0f : 0.0f, 0.0f}); + return true; + case SDL_CONTROLLERAXISMOTION: + QueueInput({InputKind::Axis, generation, event.caxis.axis, + static_cast(event.caxis.value), 0.0f}); + return true; + case SDL_MOUSEMOTION: + QueueInput({InputKind::MousePosition, generation, 0, static_cast(event.motion.x), + static_cast(event.motion.y)}); + return true; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + QueueInput({InputKind::MouseButton, generation, event.button.button, + event.type == SDL_MOUSEBUTTONDOWN ? 1.0f : 0.0f, 0.0f}); + return true; + case SDL_MOUSEWHEEL: + QueueInput({InputKind::MouseWheel, generation, 0, static_cast(event.wheel.x), + static_cast(event.wheel.y)}); + return true; + default: return false; + } +} + +struct ImeDialogOverlay::Impl { + explicit Impl(GraphicContext& context): graphics(context) {} + + ~Impl() { + ReleaseVulkan(); + if (imgui_context != nullptr) { + ImGui::DestroyContext(imgui_context); + } + } + + void EnsureContext() { + if (imgui_context != nullptr) { + ImGui::SetCurrentContext(imgui_context); + return; + } + IMGUI_CHECKVERSION(); + imgui_context = ImGui::CreateContext(); + ImGui::SetCurrentContext(imgui_context); + auto& io = ImGui::GetIO(); + io.IniFilename = nullptr; + io.LogFilename = nullptr; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + io.BackendPlatformName = "Kyty ImeDialog input"; + ImGui::StyleColorsDark(); + auto& style = ImGui::GetStyle(); + style.WindowRounding = 10.0f; + style.FrameRounding = 6.0f; + style.ItemSpacing = {8.0f, 8.0f}; + } + + void EnsureVulkan(vk::Format format, uint32_t image_count) { + EnsureContext(); + if (vulkan_initialized) { + return; + } + EXIT_IF(image_count < 2); + EXIT_IF(!ImGui_ImplVulkan_LoadFunctions(VK_API_VERSION_1_3, LoadVulkanFunction, &graphics)); + + const VkFormat color_format = static_cast(format); + ImGui_ImplVulkan_InitInfo info {}; + info.ApiVersion = VK_API_VERSION_1_3; + info.Instance = static_cast(graphics.instance); + info.PhysicalDevice = static_cast(graphics.physical_device); + info.Device = static_cast(graphics.device); + info.QueueFamily = graphics.queue_family; + info.Queue = static_cast(graphics.queue); + info.DescriptorPoolSize = IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE; + info.MinImageCount = image_count; + info.ImageCount = image_count; + info.UseDynamicRendering = true; + info.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + info.PipelineInfoMain.PipelineRenderingCreateInfo.sType = + VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1; + info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format; + info.CheckVkResultFn = CheckVulkanResult; + EXIT_IF(!ImGui_ImplVulkan_Init(&info)); + vulkan_initialized = true; + } + + void DrainInput(uint64_t generation) { + std::deque events; + bool reset = false; + { + std::scoped_lock lock(g_input_mutex); + events.swap(g_input_events); + reset = g_input_reset_requested; + g_input_reset_requested = false; + } + auto& io = ImGui::GetIO(); + if (reset) { + io.ClearEventsQueue(); + io.ClearInputKeys(); + io.ClearInputMouse(); + right_stick = {}; + } + for (const auto& event: events) { + if (event.generation != generation) { + continue; + } + switch (event.kind) { + case InputKind::Button: { + const bool down = event.x != 0.0f; + if (down && event.id == SDL_CONTROLLER_BUTTON_B) { + Ime::HostCancel(generation); + } else if (down && event.id == SDL_CONTROLLER_BUTTON_Y) { + Ime::HostBackspace(generation); + } + const ImGuiKey key = ControllerButtonToKey(event.id); + if (key != ImGuiKey_None) { + io.AddKeyEvent(key, down); + } + break; + } + case InputKind::Axis: { + const float value = event.x / (event.x < 0.0f ? 32768.0f : 32767.0f); + if (event.id == SDL_CONTROLLER_AXIS_LEFTX) { + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, value < -0.25f, + std::max(-value, 0.0f)); + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickRight, value > 0.25f, + std::max(value, 0.0f)); + } else if (event.id == SDL_CONTROLLER_AXIS_LEFTY) { + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickUp, value < -0.25f, + std::max(-value, 0.0f)); + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickDown, value > 0.25f, + std::max(value, 0.0f)); + } else if (event.id == SDL_CONTROLLER_AXIS_RIGHTX) { + right_stick.x = std::abs(value) > 0.2f ? value : 0.0f; + } else if (event.id == SDL_CONTROLLER_AXIS_RIGHTY) { + right_stick.y = std::abs(value) > 0.2f ? value : 0.0f; + } + break; + } + case InputKind::MousePosition: io.AddMousePosEvent(event.x, event.y); break; + case InputKind::MouseButton: { + int button = -1; + if (event.id == SDL_BUTTON_LEFT) button = 0; + if (event.id == SDL_BUTTON_RIGHT) button = 1; + if (event.id == SDL_BUTTON_MIDDLE) button = 2; + if (button >= 0) io.AddMouseButtonEvent(button, event.x != 0.0f); + break; + } + case InputKind::MouseWheel: io.AddMouseWheelEvent(event.x, event.y); break; + case InputKind::ResetController: + io.ClearEventsQueue(); + io.ClearInputKeys(); + right_stick = {}; + break; + } + } + } + + void KeyButton(std::string_view label, char16_t value, uint64_t generation, float width, + bool default_focus) { + const bool pressed = ImGui::Button(label.data(), {width, button_height}); + if (default_focus) { + ImGui::SetItemDefaultFocus(); + } + if (pressed) { + Ime::HostInsertText(generation, std::u16string_view(&value, 1)); + } + } + + void DrawKeyRows(const Ime::HostSnapshot& snapshot, float width) { + static constexpr std::array lower = {"1234567890", "qwertyuiop", + "asdfghjkl", "zxcvbnm"}; + static constexpr std::array upper = {"1234567890", "QWERTYUIOP", + "ASDFGHJKL", "ZXCVBNM"}; + static constexpr std::array symbols = {"1234567890", "!@#$%^&*()", + "-_=+/:?."}; + static constexpr std::array number = {"123", "456", "789", "-0."}; + + std::span rows; + if (snapshot.type == Ime::Type::Number) { + rows = number; + } else if (symbol_mode) { + rows = symbols; + } else { + rows = shift ? std::span(upper) + : std::span(lower); + } + bool first = focus_pending; + size_t row_index = 0; + for (const auto row: rows) { + const float key_width = + std::min(56.0f * ui_scale, (width - 8.0f * (row.size() - 1)) / row.size()); + const float row_width = key_width * row.size() + 8.0f * (row.size() - 1); + ImGui::SetCursorPosX((width - row_width) * 0.5f); + ImGui::PushID(static_cast(row_index++)); + for (size_t i = 0; i < row.size(); i++) { + ImGui::PushID(static_cast(i)); + const char label[2] = {row[i], '\0'}; + KeyButton(label, static_cast(row[i]), snapshot.generation, key_width, + first); + first = false; + ImGui::PopID(); + if (i + 1 < row.size()) ImGui::SameLine(); + } + ImGui::PopID(); + } + focus_pending = false; + } + + void DrawDialog(const Ime::HostSnapshot& snapshot, vk::Extent2D extent) { + const ImVec2 display(static_cast(extent.width), static_cast(extent.height)); + const bool over_2k = (snapshot.option & Ime::OPTION_USE_OVER_2K) != 0; + const float reference_width = over_2k ? 3840.0f : 1920.0f; + const float reference_height = over_2k ? 2160.0f : 1080.0f; + const float scale_x = display.x / reference_width; + const float scale_y = display.y / reference_height; + const float width = static_cast(snapshot.panel_width) * scale_x; + const float height = static_cast(snapshot.panel_height) * scale_y; + ui_scale = std::min(scale_x, scale_y) * (over_2k ? 2.0f : 1.0f); + button_height = std::max(28.0f, 42.0f * ui_scale); + + const ImVec2 pivot {AlignmentPivot(snapshot.horizontal_alignment), + AlignmentPivot(snapshot.vertical_alignment)}; + if ((snapshot.option & Ime::OPTION_FIXED_POSITION) == 0) { + const float movement = 600.0f * ImGui::GetIO().DeltaTime; + panel_offset.x += right_stick.x * movement; + panel_offset.y += right_stick.y * movement; + } + const ImVec2 base_position {snapshot.posx * scale_x - pivot.x * width, + snapshot.posy * scale_y - pivot.y * height}; + ImVec2 position {base_position.x + panel_offset.x, base_position.y + panel_offset.y}; + if ((snapshot.option & Ime::OPTION_DISABLE_POSITION_ADJ) == 0) { + position.x = std::clamp(position.x, 0.0f, std::max(display.x - width, 0.0f)); + position.y = std::clamp(position.y, 0.0f, std::max(display.y - height, 0.0f)); + panel_offset = {position.x - base_position.x, position.y - base_position.y}; + } + ImGui::SetNextWindowPos(position, ImGuiCond_Always); + ImGui::SetNextWindowSize({width, height}, ImGuiCond_Always); + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings; + ImGui::Begin("##ImeDialog", nullptr, flags); + + const std::u16string title = snapshot.title.empty() ? u"Enter text" : snapshot.title; + if (!snapshot.key_panel_visible) { + std::string compact = Common::Utf16ToUtf8(title.c_str()) + ": "; + const float glyph_width = std::max(ImGui::CalcTextSize("M").x, 1.0f); + const float text_width = + std::max(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(compact.c_str()).x, + glyph_width); + const size_t visible_units = + std::max(6, static_cast(text_width / glyph_width)); + compact += snapshot.text.empty() && !snapshot.placeholder.empty() + ? Common::Utf16ToUtf8(snapshot.placeholder.c_str()) + : VisibleText(snapshot, visible_units); + ImGui::TextUnformatted(compact.c_str()); + const float compact_height = std::min(button_height, 28.0f * ui_scale); + if (ImGui::Button("Cancel", {90.0f * ui_scale, compact_height})) { + Ime::HostCancel(snapshot.generation); + } + ImGui::SameLine(); + if (ImGui::Button(EnterLabel(snapshot.enter_label), + {90.0f * ui_scale, compact_height})) { + Ime::HostAccept(snapshot.generation); + } + ImGui::End(); + return; + } + + ImGui::TextUnformatted(Common::Utf16ToUtf8(title.c_str()).c_str()); + ImGui::Separator(); + const float text_height = std::max( + 32.0f, ((snapshot.option & Ime::OPTION_MULTILINE) != 0 ? 80.0f : 48.0f) * ui_scale); + ImGui::BeginChild("##ImeText", {0.0f, text_height}, true); + if (snapshot.text.empty() && !snapshot.placeholder.empty()) { + ImGui::TextDisabled("%s", Common::Utf16ToUtf8(snapshot.placeholder.c_str()).c_str()); + } else { + const float glyph_width = std::max(ImGui::CalcTextSize("M").x, 1.0f); + const size_t columns = std::max( + 8, static_cast(ImGui::GetContentRegionAvail().x / glyph_width)); + const size_t lines = + std::max(1, static_cast(ImGui::GetContentRegionAvail().y / + ImGui::GetTextLineHeightWithSpacing())); + const auto text = VisibleText(snapshot, columns * lines); + ImGui::TextWrapped("%s", text.c_str()); + } + ImGui::EndChild(); + ImGui::TextDisabled("%zu / %u", snapshot.text.size(), snapshot.max_text_length); + ImGui::Separator(); + + const float content_width = ImGui::GetContentRegionAvail().x; + DrawKeyRows(snapshot, content_width); + if (snapshot.type != Ime::Type::Number) { + if (ImGui::Button(shift ? "Lower" : "Shift", {84.0f * ui_scale, button_height})) { + shift = !shift; + } + ImGui::SameLine(); + if (ImGui::Button(symbol_mode ? "ABC" : "Symbols", {84.0f * ui_scale, button_height})) { + symbol_mode = !symbol_mode; + } + ImGui::SameLine(); + if (ImGui::Button("Space", {120.0f * ui_scale, button_height})) { + Ime::HostInsertText(snapshot.generation, u" "); + } + ImGui::SameLine(); + } + const float action_width = snapshot.type == Ime::Type::Number + ? std::max((content_width - 16.0f) / 3.0f, 48.0f) + : 100.0f * ui_scale; + if (ImGui::Button("Backspace", {action_width, button_height})) { + Ime::HostBackspace(snapshot.generation); + } + ImGui::SameLine(); + if ((snapshot.option & Ime::OPTION_MULTILINE) != 0) { + if (ImGui::Button("Newline", {action_width, button_height})) { + Ime::HostInsertText(snapshot.generation, u"\n"); + } + ImGui::SameLine(); + } + if (ImGui::Button("Cancel", {action_width, button_height})) { + Ime::HostCancel(snapshot.generation); + } + ImGui::SameLine(); + if (ImGui::Button(EnterLabel(snapshot.enter_label), {action_width, button_height})) { + Ime::HostAccept(snapshot.generation); + } + ImGui::End(); + } + + bool PrepareFrame(vk::Extent2D frame_extent, vk::Format format, uint32_t image_count) { + Ime::HostSnapshot snapshot; + if (!Ime::GetHostSnapshot(&snapshot)) { + return false; + } + const uint64_t prepared_generation = snapshot.generation; + EnsureVulkan(format, image_count); + if (generation != snapshot.generation) { + generation = snapshot.generation; + focus_pending = true; + shift = (snapshot.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0; + symbol_mode = false; + panel_offset = {}; + right_stick = {}; + auto& io = ImGui::GetIO(); + io.ClearEventsQueue(); + io.ClearInputKeys(); + io.ClearInputMouse(); + } + DrainInput(snapshot.generation); + + auto& io = ImGui::GetIO(); + io.DisplaySize = {static_cast(frame_extent.width), + static_cast(frame_extent.height)}; + const auto now = std::chrono::steady_clock::now(); + io.DeltaTime = last_frame == std::chrono::steady_clock::time_point {} + ? 1.0f / 60.0f + : std::clamp(std::chrono::duration(now - last_frame).count(), + 1.0f / 1000.0f, 0.1f); + last_frame = now; + ImGui_ImplVulkan_NewFrame(); + ImGui::NewFrame(); + if (!Ime::GetHostSnapshot(&snapshot) || snapshot.generation != prepared_generation) { + ImGui::EndFrame(); + return false; + } + DrawDialog(snapshot, frame_extent); + ImGui::Render(); + extent = frame_extent; + return true; + } + + void Record(vk::CommandBuffer command, vk::ImageView target) { + vk::RenderingAttachmentInfo color {}; + color.sType = vk::StructureType::eRenderingAttachmentInfo; + color.imageView = target; + color.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + color.loadOp = vk::AttachmentLoadOp::eLoad; + color.storeOp = vk::AttachmentStoreOp::eStore; + vk::RenderingInfo rendering {}; + rendering.sType = vk::StructureType::eRenderingInfo; + rendering.renderArea.extent = extent; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &color; + command.beginRendering(rendering); + { + Common::LockGuard queue_lock(graphics.queue_mutex); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), + static_cast(command)); + } + command.endRendering(); + } + + void ReleaseVulkan() { + if (!vulkan_initialized) { + return; + } + ImGui::SetCurrentContext(imgui_context); + ImGui_ImplVulkan_Shutdown(); + vulkan_initialized = false; + } + + GraphicContext& graphics; + ImGuiContext* imgui_context = nullptr; + bool vulkan_initialized = false; + bool shift = false; + bool symbol_mode = false; + bool focus_pending = true; + float ui_scale = 1.0f; + float button_height = 42.0f; + ImVec2 panel_offset {}; + ImVec2 right_stick {}; + uint64_t generation = 0; + vk::Extent2D extent {}; + std::chrono::steady_clock::time_point last_frame; +}; + +ImeDialogOverlay::ImeDialogOverlay(GraphicContext& graphics) + : m_impl(std::make_unique(graphics)) {} + +ImeDialogOverlay::~ImeDialogOverlay() = default; + +bool ImeDialogOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) { + return m_impl->PrepareFrame(extent, format, image_count); +} + +void ImeDialogOverlay::Record(vk::CommandBuffer command, vk::ImageView target) { + m_impl->Record(command, target); +} + +void ImeDialogOverlay::ReleaseVulkan() { + m_impl->ReleaseVulkan(); +} + +} // namespace Libs::Graphics diff --git a/src/graphics/presentation/imeDialogOverlay.h b/src/graphics/presentation/imeDialogOverlay.h new file mode 100644 index 0000000..7aeafee --- /dev/null +++ b/src/graphics/presentation/imeDialogOverlay.h @@ -0,0 +1,36 @@ +#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_ +#define EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_ + +#include "common/common.h" +#include "graphics/host_gpu/vulkanCommon.h" + +#include + +union SDL_Event; + +namespace Libs::Graphics { + +struct GraphicContext; + +void InitializeImeDialogInput(); +void ShutdownImeDialogInput(); +bool ProcessImeDialogInput(const SDL_Event& event); + +class ImeDialogOverlay final { +public: + explicit ImeDialogOverlay(GraphicContext& graphics); + ~ImeDialogOverlay(); + KYTY_CLASS_NO_COPY(ImeDialogOverlay); + + [[nodiscard]] bool PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count); + void Record(vk::CommandBuffer command, vk::ImageView target); + void ReleaseVulkan(); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace Libs::Graphics + +#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEDIALOGOVERLAY_H_ diff --git a/src/graphics/presentation/presenter.h b/src/graphics/presentation/presenter.h index 78e499e..451e1ad 100644 --- a/src/graphics/presentation/presenter.h +++ b/src/graphics/presentation/presenter.h @@ -25,6 +25,7 @@ public: CommandBuffer* producer = nullptr); [[nodiscard]] Frame* PrepareLastFrame(); [[nodiscard]] bool IsGuestPaused() const noexcept; + [[nodiscard]] bool NeedsImeRefresh() const noexcept; [[nodiscard]] RenderContext& Renderer() const noexcept; void Present(Frame& frame, bool reuse = false); void Discard(Frame& frame); diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 5972119..ba4347a 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -817,7 +817,24 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) { } VblankBegin(); - const bool presented = m_flip_queue.Flip(0); + bool presented = m_flip_queue.Flip(0); + if (!presented && m_presenter.NeedsImeRefresh()) { + if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) { + m_presenter.Present(*frame, true); + presented = true; + } else { + uint32_t width = 0; + uint32_t height = 0; + { + Common::LockGuard lock(m_mutex); + width = m_video_out_ctx[0].width; + height = m_video_out_ctx[0].height; + } + auto& blank = m_presenter.PrepareBlankFrame(width, height, true); + m_presenter.Present(blank); + presented = true; + } + } if (!presented && total_wait < 0) { bool any_open = false; uint32_t width = 0; diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index d2be5fd..13a8af1 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -31,11 +31,13 @@ #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" +#include "graphics/presentation/imeDialogOverlay.h" #include "graphics/presentation/presenter.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/videoOut.h" #include "graphics/presentation/window/windowInternal.h" #include "libs/controller.h" +#include "libs/imeDialog.h" #include "loader/systemContent.h" #include @@ -329,8 +331,9 @@ public: void Create(); void Recreate(bool surface_lost = false); [[nodiscard]] Status AcquireNextImage(); - void RecordPresentCommands(CommandBuffer& command, VulkanImage& source); - void Submit(CommandBuffer& command); + [[nodiscard]] bool PrepareImeOverlay(); + void RecordPresentCommands(CommandBuffer& command, VulkanImage& source, bool draw_ime_overlay); + void Submit(CommandBuffer& command); [[nodiscard]] Status Present(); [[nodiscard]] uint32_t ImageCount() const noexcept { @@ -342,16 +345,17 @@ private: void Destroy(); void RefreshSurfaceSize(); - WindowContext& m_window; - vk::SwapchainKHR m_handle = nullptr; - vk::Format m_format = vk::Format::eUndefined; - vk::Extent2D m_extent {}; - std::vector m_images; - std::vector m_image_views; - std::vector m_image_acquired; - std::vector m_render_complete; - uint32_t m_image_index = static_cast(-1); - uint32_t m_frame_index = 0; + WindowContext& m_window; + vk::SwapchainKHR m_handle = nullptr; + vk::Format m_format = vk::Format::eUndefined; + vk::Extent2D m_extent {}; + std::vector m_images; + std::vector m_image_views; + std::vector m_image_acquired; + std::vector m_render_complete; + std::unique_ptr m_ime_overlay; + uint32_t m_image_index = static_cast(-1); + uint32_t m_frame_index = 0; }; struct Presenter::Impl { @@ -389,11 +393,12 @@ struct Presenter::Impl { return image; } - RenderContext& renderer; - WindowContext& window; - Swapchain swapchain; - CommandScheduler present_scheduler; - FramePool frames; + RenderContext& renderer; + WindowContext& window; + Swapchain swapchain; + CommandScheduler present_scheduler; + FramePool frames; + std::atomic presented_ime_revision {0}; }; void Swapchain::Create() { @@ -522,6 +527,9 @@ void Swapchain::Destroy() { Common::LockGuard queue_lock(graphics.queue_mutex); RequireVulkanSuccess(graphics.queue.waitIdle(), "wait for swapchain queue"); } + if (m_ime_overlay != nullptr) { + m_ime_overlay->ReleaseVulkan(); + } for (const auto semaphore: m_image_acquired) { if (semaphore != nullptr) { @@ -606,7 +614,15 @@ Swapchain::Status Swapchain::AcquireNextImage() { return Status::Success; } -void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source) { +bool Swapchain::PrepareImeOverlay() { + if (m_ime_overlay == nullptr) { + m_ime_overlay = std::make_unique(m_window.graphic_ctx); + } + return m_ime_overlay->PrepareFrame(m_extent, m_format, ImageCount()); +} + +void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source, + bool draw_ime_overlay) { if (source.state.layout != vk::ImageLayout::eTransferSrcOptimal) { EXIT("invalid prepared presentation image, vk_image=%p layout=%d\n", static_cast(source.image), static_cast(source.state.layout)); @@ -652,11 +668,14 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc vk::Filter::eLinear); vk::ImageMemoryBarrier to_present {}; - to_present.sType = vk::StructureType::eImageMemoryBarrier; - to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead; - to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal; - to_present.newLayout = vk::ImageLayout::ePresentSrcKHR; + to_present.sType = vk::StructureType::eImageMemoryBarrier; + to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + to_present.dstAccessMask = draw_ime_overlay ? vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite + : vk::AccessFlagBits::eMemoryRead; + to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal; + to_present.newLayout = draw_ime_overlay ? vk::ImageLayout::eColorAttachmentOptimal + : vk::ImageLayout::ePresentSrcKHR; to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_present.image = m_images[m_image_index]; @@ -665,9 +684,22 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc to_present.subresourceRange.levelCount = 1; to_present.subresourceRange.baseArrayLayer = 0; to_present.subresourceRange.layerCount = 1; - vk_command.pipelineBarrier( - vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eAllCommands, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present); + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + draw_ime_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput + : vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, + &to_present); + if (draw_ime_overlay) { + m_ime_overlay->Record(vk_command, m_image_views[m_image_index]); + to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead; + to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; + to_present.newLayout = vk::ImageLayout::ePresentSrcKHR; + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, + &to_present); + } command.End(); } @@ -772,6 +804,12 @@ bool Presenter::IsGuestPaused() const noexcept { return m_impl->window.loop.paused.load(std::memory_order_acquire); } +bool Presenter::NeedsImeRefresh() const noexcept { + const auto visual = Dialog::ImeDialog::GetVisualState(); + return visual.active || + visual.revision != m_impl->presented_ime_revision.load(std::memory_order_acquire); +} + RenderContext& Presenter::Renderer() const noexcept { return m_impl->renderer; } @@ -803,7 +841,8 @@ void Presenter::Present(Frame& frame, bool reuse) { m_impl->RecoverSwapchain(Swapchain::Status::Recreate); } - auto& swapchain = m_impl->swapchain; + const auto ime_visual = Dialog::ImeDialog::GetVisualState(); + auto& swapchain = m_impl->swapchain; for (uint32_t attempt = 0; attempt < 2; attempt++) { auto status = swapchain.AcquireNextImage(); if (status != Swapchain::Status::Success) { @@ -816,8 +855,9 @@ void Presenter::Present(Frame& frame, bool reuse) { { Common::LockGuard render_lock(m_impl->renderer.GetMutex()); frame.present_commands->WaitForFenceAndReset(); - auto& command = *frame.present_commands; - swapchain.RecordPresentCommands(command, frame.image); + auto& command = *frame.present_commands; + const bool draw_ime_overlay = ime_visual.active && swapchain.PrepareImeOverlay(); + swapchain.RecordPresentCommands(command, frame.image, draw_ime_overlay); swapchain.Submit(command); } status = swapchain.Present(); @@ -827,6 +867,7 @@ void Presenter::Present(Frame& frame, bool reuse) { } RenderDocOnPresent(); + m_impl->presented_ime_revision.store(ime_visual.revision, std::memory_order_release); window.UpdateTitle(); m_impl->frames.Release(&frame, true); return; diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index f8e5410..73eb693 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -31,6 +31,7 @@ #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" +#include "graphics/presentation/imeDialogOverlay.h" #include "graphics/presentation/presenter.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/videoOut.h" @@ -1008,6 +1009,7 @@ void WindowContext::RecreateSurface() { } WindowContext::~WindowContext() { + ShutdownImeDialogInput(); presenter.reset(); LibKernel::Memory::InstallGpuResources(nullptr); render_context.reset(); diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 89793eb..5fc94b8 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -31,6 +31,7 @@ #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" +#include "graphics/presentation/imeDialogOverlay.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/window/hostInput.h" #include "graphics/presentation/window/windowInternal.h" @@ -476,6 +477,9 @@ void WindowContext::ProcessEvent(double time_s) { auto& game = loop; auto* event = &game.event; EXIT_IF(SDL_GetEventState(SDL_DISPLAYEVENT) != SDL_ENABLE); + if (ProcessImeDialogInput(*event)) { + return; + } switch (event->type) { case SDL_QUIT: GameEventQuit(game); break; @@ -787,6 +791,7 @@ static void WindowCreate(WindowContext& context) { EXIT("%s\n", SDL_GetError()); } HostInputInit(); + InitializeImeDialogInput(); LOGF("WindowCreate(): width = %d, height = %d\n", width, height); diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index 8532442..6ccd3d2 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -56,6 +56,7 @@ public: void Disconnect(int id); void Button(int id, uint32_t button, bool down); void Axis(int id, Axis axis, int value); + void ResetInputState(); void GetConnectionInfo(bool* flag, int* count); void ReadState(ControllerState* state, bool* flag, int* count); int ReadStates(ControllerState* states, int states_num, bool* flag, int* count); @@ -256,6 +257,15 @@ void GameController::Axis(int id, Controller::Axis axis, int value) { } } +void GameController::ResetInputState() { + Common::LockGuard lock(m_mutex); + ControllerState state {}; + state.time = LibKernel::KernelGetProcessTime(); + m_states_num = 0; + m_first_state = 0; + AddState(state); +} + void GameController::GetConnectionInfo(bool* flag, int* count) { EXIT_IF(flag == nullptr); EXIT_IF(count == nullptr); @@ -334,6 +344,11 @@ void ControllerAxis(int id, Axis axis, int value) { g_controller->Axis(id, axis, value); } +void ControllerResetInputState() { + EXIT_IF(g_controller == nullptr); + g_controller->ResetInputState(); +} + int KYTY_SYSV_ABI PadInit() { PRINT_NAME(); diff --git a/src/libs/controller.h b/src/libs/controller.h index 46224fb..631b112 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -53,6 +53,7 @@ void ControllerConnect(int id); void ControllerDisconnect(int id); void ControllerButton(int id, uint32_t button, bool down); void ControllerAxis(int id, Axis axis, int value); +void ControllerResetInputState(); int KYTY_SYSV_ABI PadInit(); int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param); diff --git a/src/libs/dialog.cpp b/src/libs/dialog.cpp index 7f657b5..5b0fc93 100644 --- a/src/libs/dialog.cpp +++ b/src/libs/dialog.cpp @@ -23,20 +23,6 @@ int KYTY_SYSV_ABI CommonDialogInitialize() { } // namespace CommonDialog -namespace ImeDialog { - -LIB_NAME("ImeDialog", "ImeDialog"); - -constexpr int IME_STATUS_NONE = 0; - -int KYTY_SYSV_ABI ImeDialogGetStatus() { - PRINT_NAME(); - - return IME_STATUS_NONE; -} - -} // namespace ImeDialog - namespace LoginDialog { LIB_NAME("LoginDialog", "LoginDialog"); diff --git a/src/libs/dialog.h b/src/libs/dialog.h index b187ae6..83be8ff 100644 --- a/src/libs/dialog.h +++ b/src/libs/dialog.h @@ -12,12 +12,6 @@ int KYTY_SYSV_ABI CommonDialogInitialize(); } // namespace CommonDialog -namespace ImeDialog { - -int KYTY_SYSV_ABI ImeDialogGetStatus(); - -} // namespace ImeDialog - namespace LoginDialog { int KYTY_SYSV_ABI LoginDialogInitialize(); diff --git a/src/libs/imeDialog.cpp b/src/libs/imeDialog.cpp new file mode 100644 index 0000000..568f78e --- /dev/null +++ b/src/libs/imeDialog.cpp @@ -0,0 +1,781 @@ +#include "libs/imeDialog.h" + +#include "libs/errno.h" + +#include +#include +#include +#include +#include +#include + +namespace Libs::Dialog::ImeDialog { + +namespace { + +constexpr int Error(uint32_t value) { + return static_cast(value); +} + +constexpr int ERROR_BUSY = Error(0x80bc0001); +constexpr int ERROR_INVALID_USER_ID = Error(0x80bc0010); +constexpr int ERROR_INVALID_TYPE = Error(0x80bc0011); +constexpr int ERROR_INVALID_LANGUAGES = Error(0x80bc0012); +constexpr int ERROR_INVALID_ENTER_LABEL = Error(0x80bc0013); +constexpr int ERROR_INVALID_INPUT_METHOD = Error(0x80bc0014); +constexpr int ERROR_INVALID_OPTION = Error(0x80bc0015); +constexpr int ERROR_INVALID_MAX_TEXT_LENGTH = Error(0x80bc0016); +constexpr int ERROR_INVALID_TEXT_BUFFER = Error(0x80bc0017); +constexpr int ERROR_INVALID_POSX = Error(0x80bc0018); +constexpr int ERROR_INVALID_POSY = Error(0x80bc0019); +constexpr int ERROR_INVALID_HALIGN = Error(0x80bc001a); +constexpr int ERROR_INVALID_VALIGN = Error(0x80bc001b); +constexpr int ERROR_INVALID_EXTENDED = Error(0x80bc001c); +constexpr int ERROR_INVALID_PARAM = Error(0x80bc0030); +constexpr int ERROR_INVALID_ADDRESS = Error(0x80bc0031); +constexpr int ERROR_INVALID_RESERVED = Error(0x80bc0032); +constexpr int ERROR_INVALID_TITLE = Error(0x80bc0101); +constexpr int ERROR_NOT_RUNNING = Error(0x80bc0105); +constexpr int ERROR_NOT_FINISHED = Error(0x80bc0106); +constexpr int ERROR_NOT_IN_USE = Error(0x80bc0107); + +constexpr uint32_t VALID_OPTIONS = 0x00007bff; +constexpr uint32_t VALID_EXTENDED_OPTIONS = 0x00005fde; +constexpr uint32_t VALID_EXT_KEYBOARD_MODE = 0x1c000003; +constexpr uint64_t VALID_LANGUAGES = 0x00000001ff1fffffULL; + +struct State { + Status status = Status::None; + EndStatus end_status = EndStatus::Ok; + uint64_t generation = 0; + uint64_t revision = 0; + Param param {}; + ExtendedParam extended {}; + bool input_changed = false; + bool commit_pending = false; + uint32_t cursor = 0; + std::u16string original_text; + std::u16string current_text; + std::u16string title; + std::u16string placeholder; + std::vector external_inputs; +}; + +std::mutex g_mutex; +State g_state; +std::atomic g_status {Status::None}; +std::atomic g_revision {0}; +std::atomic g_visibility_callback {nullptr}; + +bool AllZero(const int8_t* data, size_t size) { + return std::all_of(data, data + size, [](int8_t value) { return value == 0; }); +} + +bool IsValidUtf16(std::u16string_view text); + +bool ReadBounded(const char16_t* text, uint32_t limit, std::u16string* out) { + out->clear(); + if (text == nullptr) { + return true; + } + out->reserve(limit); + for (uint32_t i = 0; i <= limit; ++i) { + const char16_t value = text[i]; + if (value == u'\0') { + return IsValidUtf16(*out); + } + if (i == limit) { + return false; + } + out->push_back(value); + } + return false; +} + +bool IsValidUtf16(std::u16string_view text) { + for (size_t i = 0; i < text.size(); i++) { + const char16_t current = text[i]; + if (current >= 0xd800 && current <= 0xdbff) { + if (++i >= text.size() || text[i] < 0xdc00 || text[i] > 0xdfff) { + return false; + } + } else if (current >= 0xdc00 && current <= 0xdfff) { + return false; + } + } + return true; +} + +bool IsAllowedInput(char16_t value, Type type, uint32_t option) { + if (value == u'\r') { + return false; + } + if (value == u'\n') { + return (option & OPTION_MULTILINE) != 0; + } + if (value == u'\0') { + return false; + } + if (type == Type::Number) { + return (value >= u'0' && value <= u'9') || value == u',' || value == u'-' || value == u'.'; + } + if (type == Type::BasicLatin) { + return value >= u' ' && value <= u'~'; + } + return true; +} + +char16_t HidCharacter(uint16_t keycode, uint32_t status) { + const bool shift = (status & 0x00002200) != 0; + const bool caps = (status & 0x00020000) != 0; + if (keycode >= 4 && keycode <= 29) { + const bool upper = shift != caps; + return static_cast((upper ? u'A' : u'a') + keycode - 4); + } + if (keycode >= 30 && keycode <= 39) { + static constexpr char16_t plain[] = u"1234567890"; + static constexpr char16_t shifted[] = u"!@#$%^&*()"; + return (shift ? shifted : plain)[keycode - 30]; + } + if (keycode == 44) { + return u' '; + } + if (keycode >= 45 && keycode <= 56) { + static constexpr char16_t plain[] = u"-=[]\\#;'`,./"; + static constexpr char16_t shifted[] = u"_+{}|~:\"~<>?"; + return (shift ? shifted : plain)[keycode - 45]; + } + if (keycode >= 89 && keycode <= 98) { + static constexpr char16_t keypad[] = u"1234567890"; + return keypad[keycode - 89]; + } + if (keycode == 99) { + return u'.'; + } + return u'\0'; +} + +bool ApplyKeyboardFilterOutput(ExternalInput* input, uint16_t keycode, uint32_t status, + bool multiline) { + constexpr uint32_t KEYCODE_VALID = 0x00000001; + constexpr uint32_t CHARACTER_VALID = 0x00000002; + if (keycode == input->key.keycode && status == input->key.status) { + return true; + } + if ((status & KEYCODE_VALID) == 0 || keycode == 0) { + return input->action == ExternalAction::Text && (status & CHARACTER_VALID) != 0; + } + input->key.keycode = keycode; + input->key.status = status; + input->text.clear(); + switch (keycode) { + case 40: + case 88: + case 158: + input->action = multiline ? ExternalAction::Newline : ExternalAction::Accept; + break; + case 41: input->action = ExternalAction::Cancel; break; + case 42: + case 187: input->action = ExternalAction::Backspace; break; + case 43: + case 186: input->action = ExternalAction::None; break; + case 79: input->action = ExternalAction::MoveRight; break; + case 80: input->action = ExternalAction::MoveLeft; break; + default: { + const char16_t character = HidCharacter(keycode, status); + if (character == u'\0') { + return false; + } + input->action = ExternalAction::Text; + input->key.character = character; + input->text.push_back(character); + break; + } + } + return true; +} + +void ClampText(std::u16string* text, uint32_t limit) { + if (text->size() <= limit) { + return; + } + text->resize(limit); + if (!text->empty() && text->back() >= 0xd800 && text->back() <= 0xdbff) { + text->pop_back(); + } +} + +uint32_t NormalizeCursor(std::u16string_view text, uint32_t cursor) { + cursor = std::min(cursor, static_cast(text.size())); + if (cursor > 0 && cursor < text.size() && text[cursor - 1] >= 0xd800 && + text[cursor - 1] <= 0xdbff && text[cursor] >= 0xdc00 && text[cursor] <= 0xdfff) { + cursor++; + } + return cursor; +} + +void WriteGuestText(const State& state, const std::u16string& text) { + if (state.param.input_text_buffer == nullptr) { + return; + } + const size_t size = std::min(text.size(), state.param.max_text_length); + std::memcpy(state.param.input_text_buffer, text.data(), size * sizeof(char16_t)); + state.param.input_text_buffer[size] = u'\0'; +} + +void NotifyVisibility(bool visible, uint64_t generation) { + if (const auto callback = g_visibility_callback.load(std::memory_order_acquire); + callback != nullptr) { + callback(visible, generation); + } +} + +int ValidateExtended(const ExtendedParam* extended) { + if (extended == nullptr) { + return OK; + } + if ((extended->option & ~VALID_EXTENDED_OPTIONS) != 0 || + ((extended->option & 0x00004000) != 0 && (extended->option & 0x00000080) == 0) || + extended->priority > 3 || extended->disable_device > 7 || + (extended->ext_keyboard_mode & ~VALID_EXT_KEYBOARD_MODE) != 0 || + !AllZero(extended->reserved, sizeof(extended->reserved))) { + return ERROR_INVALID_EXTENDED; + } + return OK; +} + +int ValidateParam(const Param* param, const ExtendedParam* extended, std::u16string* initial, + std::u16string* title, std::u16string* placeholder) { + if (param == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (static_cast(param->type) > static_cast(Type::Number)) { + return ERROR_INVALID_TYPE; + } + if ((param->option & ~VALID_OPTIONS) != 0) { + return ERROR_INVALID_OPTION; + } + if ((param->supported_languages & ~VALID_LANGUAGES) != 0) { + return ERROR_INVALID_LANGUAGES; + } + const bool over_2k = (param->option & OPTION_USE_OVER_2K) != 0; + const float max_x = over_2k ? 3840.0f : 1920.0f; + const float max_y = over_2k ? 2160.0f : 1080.0f; + if (!std::isfinite(param->posx) || param->posx < 0.0f || param->posx >= max_x) { + return ERROR_INVALID_POSX; + } + if (!std::isfinite(param->posy) || param->posy < 0.0f || param->posy >= max_y) { + return ERROR_INVALID_POSY; + } + if (static_cast(param->horizontal_alignment) > 2) { + return ERROR_INVALID_HALIGN; + } + if (static_cast(param->vertical_alignment) > 2) { + return ERROR_INVALID_VALIGN; + } + const bool multiline = (param->option & OPTION_MULTILINE) != 0; + const bool password = (param->option & OPTION_PASSWORD) != 0; + if ((multiline && password) || + (multiline && param->type != Type::Default && param->type != Type::BasicLatin) || + (password && param->type != Type::BasicLatin && param->type != Type::Number)) { + return ERROR_INVALID_PARAM; + } + if (param->user_id < 0 || param->user_id == 0xff) { + return ERROR_INVALID_USER_ID; + } + if (!AllZero(param->reserved, sizeof(param->reserved))) { + return ERROR_INVALID_RESERVED; + } + if (param->input_text_buffer == nullptr) { + return ERROR_INVALID_TEXT_BUFFER; + } + const int extended_result = ValidateExtended(extended); + if (extended_result != OK) { + return extended_result; + } + if (static_cast(param->enter_label) > static_cast(EnterLabel::Go)) { + return ERROR_INVALID_ENTER_LABEL; + } + if (param->input_method != 0) { + return ERROR_INVALID_INPUT_METHOD; + } + if (param->max_text_length == 0 || param->max_text_length > IME_DIALOG_MAX_TEXT_LENGTH) { + return ERROR_INVALID_MAX_TEXT_LENGTH; + } + if (!ReadBounded(param->input_text_buffer, param->max_text_length, initial)) { + return ERROR_INVALID_TEXT_BUFFER; + } + if (!ReadBounded(param->title, IME_DIALOG_MAX_TITLE_LENGTH, title)) { + return ERROR_INVALID_TITLE; + } + if (!ReadBounded(param->placeholder, IME_DIALOG_MAX_PLACEHOLDER_LENGTH, placeholder)) { + return ERROR_INVALID_PARAM; + } + return OK; +} + +void ComputePanelSize(const Param& param, const ExtendedParam* extended, uint32_t* width, + uint32_t* height) { + const bool multiline = (param.option & OPTION_MULTILINE) != 0; + const bool hide_keyboard = extended != nullptr && (param.option & OPTION_EXT_KEYBOARD) != 0 && + (extended->option & 0x00000400) != 0; + if (param.type == Type::Number) { + *width = 370; + *height = hide_keyboard ? 102 : 522; + } else if (param.type == Type::BasicLatin) { + *width = 793; + if (hide_keyboard) { + *height = multiline ? 203 : 103; + } else { + *height = multiline ? 628 : 528; + } + } else { + *width = 793; + if (hide_keyboard) { + *height = multiline ? 268 : 168; + } else { + *height = multiline ? 628 : 528; + } + } + if ((param.option & OPTION_USE_OVER_2K) != 0) { + *width *= 2; + *height *= 2; + } +} + +void ApplyFilterAndCommit() { + TextFilter filter = nullptr; + std::u16string source; + uint32_t max_length = 0; + uint64_t generation = 0; + uint64_t revision = 0; + { + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None || (!g_state.input_changed && !g_state.commit_pending)) { + return; + } + filter = g_state.input_changed ? g_state.param.filter : nullptr; + source = g_state.current_text; + max_length = g_state.param.max_text_length; + generation = g_state.generation; + revision = g_state.revision; + g_state.input_changed = false; + } + + if (filter != nullptr) { + std::vector output(IME_DIALOG_MAX_TEXT_LENGTH + 1, u'\0'); + uint32_t output_length = IME_DIALOG_MAX_TEXT_LENGTH; + if (filter(output.data(), &output_length, source.c_str(), + static_cast(source.size())) == 0 && + output_length <= IME_DIALOG_MAX_TEXT_LENGTH && + IsValidUtf16(std::u16string_view(output.data(), output_length))) { + source.assign(output.data(), output.data() + output_length); + ClampText(&source, max_length); + } + } + + std::scoped_lock lock(g_mutex); + if (g_state.generation != generation || g_state.revision != revision || + g_state.status == Status::None) { + if (g_state.generation == generation && + (g_state.status == Status::Running || + (g_state.status == Status::Finished && g_state.end_status == EndStatus::Ok))) { + g_state.input_changed = true; + g_state.commit_pending = true; + } + return; + } + if (!g_state.input_changed) { + g_state.current_text = std::move(source); + g_state.cursor = NormalizeCursor(g_state.current_text, g_state.cursor); + } + const auto& committed = + g_state.status == Status::Finished && g_state.end_status != EndStatus::Ok + ? g_state.original_text + : g_state.current_text; + WriteGuestText(g_state, committed); + g_state.commit_pending = false; +} + +bool MatchRunningGeneration(uint64_t generation) { + return g_state.status == Status::Running && g_state.generation == generation; +} + +bool FinishFromHost(uint64_t generation, EndStatus end_status) { + uint64_t notify_generation = 0; + { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation)) { + return false; + } + g_state.status = Status::Finished; + g_state.end_status = end_status; + g_state.commit_pending = true; + g_state.revision++; + notify_generation = g_state.generation; + g_status.store(Status::Finished, std::memory_order_release); + g_revision.store(g_state.revision, std::memory_order_release); + } + NotifyVisibility(false, notify_generation); + return true; +} + +void ApplyExternalInputs() { + std::vector inputs; + ExtKeyboardFilter filter = nullptr; + uint64_t generation = 0; + int32_t user_id = 0; + bool multiline = false; + { + std::scoped_lock lock(g_mutex); + if (g_state.status != Status::Running || g_state.external_inputs.empty()) { + return; + } + inputs.swap(g_state.external_inputs); + filter = g_state.extended.ext_keyboard_filter; + generation = g_state.generation; + user_id = g_state.param.user_id; + multiline = (g_state.param.option & OPTION_MULTILINE) != 0; + } + + for (auto& input: inputs) { + { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation)) { + break; + } + } + input.key.user_id = user_id; + bool accepted = true; + if (filter != nullptr) { + uint16_t output_keycode = input.key.keycode; + uint32_t output_status = input.key.status; + if (filter(&input.key, &output_keycode, &output_status, nullptr) == 0) { + accepted = + ApplyKeyboardFilterOutput(&input, output_keycode, output_status, multiline); + } + } + if (!accepted) { + continue; + } + switch (input.action) { + case ExternalAction::None: break; + case ExternalAction::Text: HostInsertText(generation, input.text); break; + case ExternalAction::Backspace: HostBackspace(generation); break; + case ExternalAction::MoveLeft: HostMoveCursor(generation, -1); break; + case ExternalAction::MoveRight: HostMoveCursor(generation, 1); break; + case ExternalAction::Cancel: HostCancel(generation); break; + case ExternalAction::Accept: HostAccept(generation); break; + case ExternalAction::Newline: HostInsertText(generation, u"\n"); break; + } + } +} + +} // namespace + +int KYTY_SYSV_ABI ImeDialogGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) { + return ImeDialogGetPanelSizeExtended(param, nullptr, width, height); +} + +int KYTY_SYSV_ABI ImeDialogGetPanelSizeExtended(const Param* param, const ExtendedParam* extended, + uint32_t* width, uint32_t* height) { + if (param == nullptr || width == nullptr || height == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (static_cast(param->type) > static_cast(Type::Number)) { + return ERROR_INVALID_TYPE; + } + if ((param->option & ~VALID_OPTIONS) != 0) { + return ERROR_INVALID_OPTION; + } + if ((param->supported_languages & ~VALID_LANGUAGES) != 0) { + return ERROR_INVALID_LANGUAGES; + } + const int extended_result = ValidateExtended(extended); + if (extended_result != OK) { + return extended_result; + } + ComputePanelSize(*param, extended, width, height); + return OK; +} + +int KYTY_SYSV_ABI ImeDialogInit(const Param* param, const ExtendedParam* extended) { + if (g_status.load(std::memory_order_acquire) != Status::None) { + return ERROR_BUSY; + } + + std::u16string initial; + std::u16string title; + std::u16string placeholder; + const int validation = ValidateParam(param, extended, &initial, &title, &placeholder); + if (validation != OK) { + return validation; + } + + uint64_t generation = 0; + { + std::scoped_lock lock(g_mutex); + if (g_state.status != Status::None) { + return ERROR_BUSY; + } + const uint64_t next_generation = g_state.generation + 1; + const uint64_t next_revision = g_state.revision + 1; + g_state = {}; + g_state.status = Status::Running; + g_state.generation = next_generation; + g_state.revision = next_revision; + g_state.param = *param; + if (extended != nullptr) { + g_state.extended = *extended; + } + g_state.original_text = initial; + g_state.current_text = std::move(initial); + g_state.cursor = static_cast(g_state.current_text.size()); + g_state.title = std::move(title); + g_state.placeholder = std::move(placeholder); + g_state.commit_pending = true; + generation = g_state.generation; + g_status.store(Status::Running, std::memory_order_release); + g_revision.store(g_state.revision, std::memory_order_release); + } + NotifyVisibility(true, generation); + return OK; +} + +int KYTY_SYSV_ABI ImeDialogGetStatus() { + ApplyExternalInputs(); + ApplyFilterAndCommit(); + return static_cast(g_status.load(std::memory_order_acquire)); +} + +int KYTY_SYSV_ABI ImeDialogAbort() { + uint64_t generation = 0; + { + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + if (g_state.status != Status::Running) { + return ERROR_NOT_RUNNING; + } + generation = g_state.generation; + } + if (!FinishFromHost(generation, EndStatus::Aborted)) { + return ERROR_NOT_RUNNING; + } + ApplyFilterAndCommit(); + return OK; +} + +int KYTY_SYSV_ABI ImeDialogGetResult(Result* result) { + { + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + } + if (result == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (!AllZero(result->reserved, sizeof(result->reserved))) { + return ERROR_INVALID_RESERVED; + } + { + std::scoped_lock lock(g_mutex); + if (g_state.status != Status::Finished) { + return ERROR_NOT_FINISHED; + } + } + ApplyFilterAndCommit(); + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + if (g_state.status != Status::Finished) { + return ERROR_NOT_FINISHED; + } + result->endstatus = g_state.end_status; + return OK; +} + +int KYTY_SYSV_ABI ImeDialogTerm() { + { + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + if (g_state.status != Status::Finished) { + return ERROR_NOT_FINISHED; + } + } + ApplyFilterAndCommit(); + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + if (g_state.status != Status::Finished) { + return ERROR_NOT_FINISHED; + } + const uint64_t generation = g_state.generation; + const uint64_t revision = g_state.revision; + g_state = {}; + g_state.generation = generation; + g_state.revision = revision; + g_status.store(Status::None, std::memory_order_release); + return OK; +} + +int KYTY_SYSV_ABI ImeDialogGetPanelPositionAndForm(PositionAndForm* form) { + std::scoped_lock lock(g_mutex); + if (g_state.status == Status::None) { + return ERROR_NOT_IN_USE; + } + if (form == nullptr) { + return ERROR_INVALID_ADDRESS; + } + form->type = 2; + form->posx = g_state.param.posx; + form->posy = g_state.param.posy; + form->horizontal_alignment = g_state.param.horizontal_alignment; + form->vertical_alignment = g_state.param.vertical_alignment; + ComputePanelSize(g_state.param, &g_state.extended, &form->width, &form->height); + return OK; +} + +VisualState GetVisualState() noexcept { + return {g_status.load(std::memory_order_acquire) == Status::Running, + g_revision.load(std::memory_order_acquire)}; +} + +void SetVisibilityCallback(VisibilityCallback callback) noexcept { + g_visibility_callback.store(callback, std::memory_order_release); +} + +bool GetHostSnapshot(HostSnapshot* snapshot) { + if (snapshot == nullptr) { + return false; + } + std::scoped_lock lock(g_mutex); + if (g_state.status != Status::Running) { + return false; + } + snapshot->generation = g_state.generation; + snapshot->type = g_state.param.type; + snapshot->enter_label = g_state.param.enter_label; + snapshot->option = g_state.param.option; + snapshot->max_text_length = g_state.param.max_text_length; + snapshot->cursor = g_state.cursor; + snapshot->disable_device = g_state.extended.disable_device; + snapshot->key_panel_visible = (g_state.param.option & OPTION_EXT_KEYBOARD) == 0 || + (g_state.extended.option & 0x00000400) == 0; + snapshot->posx = g_state.param.posx; + snapshot->posy = g_state.param.posy; + snapshot->horizontal_alignment = g_state.param.horizontal_alignment; + snapshot->vertical_alignment = g_state.param.vertical_alignment; + ComputePanelSize(g_state.param, &g_state.extended, &snapshot->panel_width, + &snapshot->panel_height); + snapshot->text = g_state.current_text; + snapshot->title = g_state.title; + snapshot->placeholder = g_state.placeholder; + return true; +} + +bool HostInsertText(uint64_t generation, std::u16string_view text) { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation) || text.empty()) { + return false; + } + if (!IsValidUtf16(text)) { + return false; + } + std::u16string allowed; + allowed.reserve(text.size()); + for (const char16_t value: text) { + if (IsAllowedInput(value, g_state.param.type, g_state.param.option)) { + allowed.push_back(value); + } + } + if (allowed.empty()) { + return false; + } + const size_t available = g_state.param.max_text_length - g_state.current_text.size(); + if (available == 0) { + return false; + } + std::u16string insertion(std::u16string_view(allowed).substr(0, available)); + if (insertion.size() < allowed.size() && !insertion.empty() && insertion.back() >= 0xd800 && + insertion.back() <= 0xdbff) { + insertion.pop_back(); + } + if (insertion.empty()) { + return false; + } + std::u16string candidate = g_state.current_text; + candidate.insert(g_state.cursor, insertion); + g_state.current_text = std::move(candidate); + g_state.cursor += static_cast(insertion.size()); + g_state.input_changed = true; + g_state.commit_pending = true; + return true; +} + +bool HostBackspace(uint64_t generation) { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation) || g_state.cursor == 0) { + return false; + } + uint32_t first = g_state.cursor - 1; + if (first > 0 && g_state.current_text[first] >= 0xdc00 && + g_state.current_text[first] <= 0xdfff && g_state.current_text[first - 1] >= 0xd800 && + g_state.current_text[first - 1] <= 0xdbff) { + first--; + } + g_state.current_text.erase(first, g_state.cursor - first); + g_state.cursor = first; + g_state.input_changed = true; + g_state.commit_pending = true; + return true; +} + +bool HostMoveCursor(uint64_t generation, int delta) { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation) || delta == 0) { + return false; + } + int next = std::clamp(static_cast(g_state.cursor) + delta, 0, + static_cast(g_state.current_text.size())); + if (delta < 0 && next > 0 && next < static_cast(g_state.current_text.size()) && + g_state.current_text[next] >= 0xdc00 && g_state.current_text[next] <= 0xdfff && + g_state.current_text[next - 1] >= 0xd800 && g_state.current_text[next - 1] <= 0xdbff) { + next--; + } else if (delta > 0 && next > 0 && next < static_cast(g_state.current_text.size()) && + g_state.current_text[next - 1] >= 0xd800 && + g_state.current_text[next - 1] <= 0xdbff && g_state.current_text[next] >= 0xdc00 && + g_state.current_text[next] <= 0xdfff) { + next++; + } + if (next == static_cast(g_state.cursor)) { + return false; + } + g_state.cursor = static_cast(next); + return true; +} + +bool HostAccept(uint64_t generation) { + return FinishFromHost(generation, EndStatus::Ok); +} + +bool HostCancel(uint64_t generation) { + return FinishFromHost(generation, EndStatus::UserCanceled); +} + +bool HostQueueExternalInput(uint64_t generation, ExternalInput input) { + std::scoped_lock lock(g_mutex); + if (!MatchRunningGeneration(generation) || g_state.external_inputs.size() >= 128) { + return false; + } + g_state.external_inputs.push_back(std::move(input)); + return true; +} + +} // namespace Libs::Dialog::ImeDialog diff --git a/src/libs/imeDialog.h b/src/libs/imeDialog.h new file mode 100644 index 0000000..c6c5282 --- /dev/null +++ b/src/libs/imeDialog.h @@ -0,0 +1,199 @@ +#ifndef EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_ +#define EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_ + +#include "common/abi.h" + +#include +#include +#include +#include + +namespace Libs::Dialog::ImeDialog { + +constexpr uint32_t IME_DIALOG_MAX_TEXT_LENGTH = 2048; +constexpr uint32_t IME_DIALOG_MAX_TITLE_LENGTH = 128; +constexpr uint32_t IME_DIALOG_MAX_PLACEHOLDER_LENGTH = 64; + +enum class Status : uint32_t { None = 0, Running = 1, Finished = 2 }; +enum class EndStatus : uint32_t { Ok = 0, UserCanceled = 1, Aborted = 2 }; +enum class Type : uint32_t { Default = 0, BasicLatin = 1, Url = 2, Mail = 3, Number = 4 }; +enum class EnterLabel : uint32_t { Default = 0, Send = 1, Search = 2, Go = 3 }; +enum class Alignment : uint32_t { Start = 0, Center = 1, End = 2 }; + +enum Option : uint32_t { + OPTION_MULTILINE = 0x00000001, + OPTION_NO_AUTO_CAPITALIZE = 0x00000002, + OPTION_PASSWORD = 0x00000004, + OPTION_LANGUAGES_FORCED = 0x00000008, + OPTION_EXT_KEYBOARD = 0x00000010, + OPTION_NO_LEARNING = 0x00000020, + OPTION_FIXED_POSITION = 0x00000040, + OPTION_DISABLE_COPY_PASTE = 0x00000080, + OPTION_DISABLE_RESUME = 0x00000100, + OPTION_DISABLE_AUTO_SPACE = 0x00000200, + OPTION_DISABLE_POSITION_ADJ = 0x00000800, + OPTION_EXPANDED_PREEDIT = 0x00001000, + OPTION_JAPANESE_CAPS_LOCK = 0x00002000, + OPTION_USE_OVER_2K = 0x00004000, +}; + +enum DisableDevice : uint32_t { + DISABLE_DEVICE_CONTROLLER = 0x00000001, + DISABLE_DEVICE_EXT_KEYBOARD = 0x00000002, + DISABLE_DEVICE_REMOTE_OSK = 0x00000004, +}; + +struct Color { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +struct Keycode; + +using TextFilter = int32_t(KYTY_SYSV_ABI*)(char16_t* out_text, uint32_t* out_text_length, + const char16_t* source_text, + uint32_t source_text_length); +using ExtKeyboardFilter = int(KYTY_SYSV_ABI*)(const Keycode* source_keycode, uint16_t* out_keycode, + uint32_t* out_status, void* reserved); + +struct Param { + int32_t user_id; + Type type; + uint64_t supported_languages; + EnterLabel enter_label; + uint32_t input_method; + TextFilter filter; + uint32_t option; + uint32_t max_text_length; + char16_t* input_text_buffer; + float posx; + float posy; + Alignment horizontal_alignment; + Alignment vertical_alignment; + const char16_t* placeholder; + const char16_t* title; + int8_t reserved[16]; +}; + +struct Result { + EndStatus endstatus; + int8_t reserved[12]; +}; + +struct ExtendedParam { + uint32_t option; + Color color_base; + Color color_line; + Color color_text_field; + Color color_preedit; + Color color_button_default; + Color color_button_function; + Color color_button_symbol; + Color color_text; + Color color_special; + uint32_t priority; + const char* additional_dictionary_path; + ExtKeyboardFilter ext_keyboard_filter; + uint32_t disable_device; + uint32_t ext_keyboard_mode; + int8_t reserved[60]; +}; + +struct PositionAndForm { + uint32_t type; + float posx; + float posy; + Alignment horizontal_alignment; + Alignment vertical_alignment; + uint32_t width; + uint32_t height; +}; + +struct Keycode { + uint16_t keycode; + char16_t character; + uint32_t status; + uint32_t type; + int32_t user_id; + uint32_t resource_id; + uint64_t timestamp; +}; + +enum class ExternalAction : uint8_t { + None, + Text, + Backspace, + MoveLeft, + MoveRight, + Cancel, + Accept, + Newline, +}; + +struct ExternalInput { + Keycode key; + ExternalAction action; + std::u16string text; +}; + +static_assert(sizeof(Param) == 0x60); +static_assert(offsetof(Param, input_text_buffer) == 0x28); +static_assert(offsetof(Param, title) == 0x48); +static_assert(sizeof(Result) == 0x10); +static_assert(sizeof(ExtendedParam) == 0x88); +static_assert(offsetof(ExtendedParam, additional_dictionary_path) == 0x30); +static_assert(sizeof(PositionAndForm) == 0x1c); +static_assert(sizeof(Keycode) == 0x20); + +struct VisualState { + bool active; + uint64_t revision; +}; + +struct HostSnapshot { + uint64_t generation; + Type type; + EnterLabel enter_label; + uint32_t option; + uint32_t max_text_length; + uint32_t cursor; + uint32_t disable_device; + bool key_panel_visible; + float posx; + float posy; + Alignment horizontal_alignment; + Alignment vertical_alignment; + uint32_t panel_width; + uint32_t panel_height; + std::u16string text; + std::u16string title; + std::u16string placeholder; +}; + +using VisibilityCallback = void (*)(bool visible, uint64_t generation); + +int KYTY_SYSV_ABI ImeDialogGetPanelSize(const Param* param, uint32_t* width, uint32_t* height); +int KYTY_SYSV_ABI ImeDialogGetPanelSizeExtended(const Param* param, const ExtendedParam* extended, + uint32_t* width, uint32_t* height); +int KYTY_SYSV_ABI ImeDialogInit(const Param* param, const ExtendedParam* extended); +int KYTY_SYSV_ABI ImeDialogGetStatus(); +int KYTY_SYSV_ABI ImeDialogAbort(); +int KYTY_SYSV_ABI ImeDialogGetResult(Result* result); +int KYTY_SYSV_ABI ImeDialogTerm(); +int KYTY_SYSV_ABI ImeDialogGetPanelPositionAndForm(PositionAndForm* form); + +VisualState GetVisualState() noexcept; +void SetVisibilityCallback(VisibilityCallback callback) noexcept; +bool GetHostSnapshot(HostSnapshot* snapshot); +bool HostInsertText(uint64_t generation, std::u16string_view text); +bool HostBackspace(uint64_t generation); +bool HostMoveCursor(uint64_t generation, int delta); +bool HostAccept(uint64_t generation); +bool HostCancel(uint64_t generation); +bool HostQueueExternalInput(uint64_t generation, ExternalInput input); + +} // namespace Libs::Dialog::ImeDialog + +#endif // EMULATOR_INCLUDE_EMULATOR_LIBS_IMEDIALOG_H_ diff --git a/src/libs/libDialog.cpp b/src/libs/libDialog.cpp index 32a1057..3dedc3a 100644 --- a/src/libs/libDialog.cpp +++ b/src/libs/libDialog.cpp @@ -1,5 +1,6 @@ #include "common/abi.h" #include "libs/dialog.h" +#include "libs/imeDialog.h" #include "libs/libs.h" #include "loader/symbolDatabase.h" @@ -24,7 +25,14 @@ LIB_VERSION("ImeDialog", 1, "ImeDialog", 1, 1); namespace ImeDialog = Dialog::ImeDialog; LIB_DEFINE(InitDialog_1_ImeDialog) { + LIB_FUNC("CRD+jSErEJQ", ImeDialog::ImeDialogGetPanelSizeExtended); LIB_FUNC("IADmD4tScBY", ImeDialog::ImeDialogGetStatus); + LIB_FUNC("gyTyVn+bXMw", ImeDialog::ImeDialogTerm); + LIB_FUNC("x01jxu+vxlc", ImeDialog::ImeDialogGetResult); + LIB_FUNC("wqsJvRXwl58", ImeDialog::ImeDialogGetPanelSize); + LIB_FUNC("NUeBrN7hzf0", ImeDialog::ImeDialogInit); + LIB_FUNC("oBmw4xrmfKs", ImeDialog::ImeDialogAbort); + LIB_FUNC("8jqzzPioYl8", ImeDialog::ImeDialogGetPanelPositionAndForm); } } // namespace LibImeDialog diff --git a/tests/ImeDialogTests.cpp b/tests/ImeDialogTests.cpp new file mode 100644 index 0000000..a66c276 --- /dev/null +++ b/tests/ImeDialogTests.cpp @@ -0,0 +1,320 @@ +#include "libs/imeDialog.h" + +#include +#include +#include +#include +#include + +namespace Ime = Libs::Dialog::ImeDialog; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::abort(); \ + } \ + } while (false) + +namespace { + +constexpr int Error(uint32_t value) { return static_cast(value); } + +int g_filter_calls = 0; +int g_keyboard_filter_calls = 0; +bool g_block_keyboard = false; +uint16_t g_remap_keyboard = 0; + +int32_t KYTY_SYSV_ABI CopyFilter(char16_t *out_text, uint32_t *out_length, + const char16_t *source, + uint32_t source_length) { + g_filter_calls++; + CHECK(*out_length >= source_length); + std::memcpy(out_text, source, source_length * sizeof(char16_t)); + *out_length = source_length; + return 0; +} + +int32_t KYTY_SYSV_ABI SupplementaryFilter(char16_t *out_text, + uint32_t *out_length, + const char16_t *, uint32_t) { + CHECK(*out_length >= 2); + out_text[0] = 0xd83d; + out_text[1] = 0xde00; + *out_length = 2; + return 0; +} + +int KYTY_SYSV_ABI KeyboardFilter(const Ime::Keycode *source, + uint16_t *out_keycode, uint32_t *out_status, + void *) { + g_keyboard_filter_calls++; + if (g_block_keyboard) { + *out_keycode = 0; + *out_status = 0; + return 0; + } + if (g_remap_keyboard != 0) { + *out_keycode = g_remap_keyboard; + *out_status = source->status; + return 0; + } + return -1; +} + +Ime::Param MakeParam(char16_t *text) { + Ime::Param param{}; + param.user_id = 1000; + param.type = Ime::Type::Default; + param.supported_languages = 1; + param.enter_label = Ime::EnterLabel::Default; + param.input_text_buffer = text; + param.max_text_length = 31; + return param; +} + +void TestLayoutAndPanelSize() { + static_assert(sizeof(Ime::Param) == 0x60); + static_assert(sizeof(Ime::ExtendedParam) == 0x88); + static_assert(sizeof(Ime::Result) == 0x10); + static_assert(sizeof(Ime::PositionAndForm) == 0x1c); + + std::array text{}; + auto param = MakeParam(text.data()); + uint32_t width = 0; + uint32_t height = 0; + CHECK(Ime::ImeDialogGetPanelSize(¶m, &width, &height) == 0); + CHECK(width == 793 && height == 528); + param.option = Ime::OPTION_MULTILINE; + CHECK(Ime::ImeDialogGetPanelSize(¶m, &width, &height) == 0); + CHECK(width == 793 && height == 628); + param.option = 0; + param.type = Ime::Type::Number; + CHECK(Ime::ImeDialogGetPanelSize(¶m, &width, &height) == 0); + CHECK(width == 370 && height == 522); + + param = MakeParam(text.data()); + param.option = Ime::OPTION_EXT_KEYBOARD; + Ime::ExtendedParam extended{}; + extended.option = 0x00000400; + CHECK(Ime::ImeDialogGetPanelSizeExtended(¶m, &extended, &width, + &height) == 0); + CHECK(width == 793 && height == 168); + param.type = Ime::Type::BasicLatin; + CHECK(Ime::ImeDialogGetPanelSizeExtended(¶m, &extended, &width, + &height) == 0); + CHECK(width == 793 && height == 103); +} + +void TestAcceptLifecycle() { + std::array text{u'A', u'l', u'i', u'c', u'e'}; + auto param = MakeParam(text.data()); + CHECK(Ime::ImeDialogInit(¶m, nullptr) == 0); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(Ime::ImeDialogInit(nullptr, nullptr) == Error(0x80bc0001)); + + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(snapshot.text == u"Alice"); + CHECK(Ime::HostInsertText(snapshot.generation, u" 2")); + CHECK(Ime::HostBackspace(snapshot.generation)); + CHECK(Ime::HostAccept(snapshot.generation)); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Finished)); + CHECK(std::u16string(text.data()) == u"Alice "); + + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(result.endstatus == Ime::EndStatus::Ok); + CHECK(Ime::ImeDialogTerm() == 0); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::None)); +} + +void TestCancelAndAbortRestoreText() { + for (const auto expected : + {Ime::EndStatus::UserCanceled, Ime::EndStatus::Aborted}) { + std::array text{u'o', u'l', u'd'}; + auto param = MakeParam(text.data()); + CHECK(Ime::ImeDialogInit(¶m, nullptr) == 0); + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(Ime::HostInsertText(snapshot.generation, u" value")); + if (expected == Ime::EndStatus::UserCanceled) { + CHECK(Ime::HostCancel(snapshot.generation)); + } else { + CHECK(Ime::ImeDialogAbort() == 0); + } + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(result.endstatus == expected); + CHECK(std::u16string(text.data()) == u"old"); + CHECK(Ime::ImeDialogTerm() == 0); + } +} + +void TestValidation() { + std::array text{}; + auto param = MakeParam(text.data()); + param.reserved[0] = 1; + CHECK(Ime::ImeDialogInit(¶m, nullptr) == Error(0x80bc0032)); + param.reserved[0] = 0; + param.max_text_length = Ime::IME_DIALOG_MAX_TEXT_LENGTH + 1; + CHECK(Ime::ImeDialogInit(¶m, nullptr) == Error(0x80bc0016)); + CHECK(Ime::ImeDialogGetResult(nullptr) == Error(0x80bc0107)); + + param = MakeParam(text.data()); + Ime::ExtendedParam extended{}; + extended.ext_keyboard_mode = 0x00000004; + CHECK(Ime::ImeDialogInit(¶m, &extended) == Error(0x80bc001c)); + extended = {}; + extended.option = 0x00000200; + extended.disable_device = + Ime::DISABLE_DEVICE_CONTROLLER | Ime::DISABLE_DEVICE_EXT_KEYBOARD; + CHECK(Ime::ImeDialogInit(¶m, &extended) == 0); + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(snapshot.disable_device == 3); + CHECK(Ime::ImeDialogAbort() == 0); + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(Ime::ImeDialogTerm() == 0); +} + +void TestFilteringAndInputPolicy() { + std::array text{u'1'}; + auto param = MakeParam(text.data()); + param.type = Ime::Type::Number; + param.filter = CopyFilter; + g_filter_calls = 0; + CHECK(Ime::ImeDialogInit(¶m, nullptr) == 0); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_filter_calls == 0); + CHECK(Ime::ImeDialogGetResult(nullptr) == Error(0x80bc0031)); + CHECK(Ime::ImeDialogTerm() == Error(0x80bc0106)); + CHECK(g_filter_calls == 0); + + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(Ime::HostInsertText(snapshot.generation, u"a2.\n")); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_filter_calls == 1); + CHECK(std::u16string(text.data()) == u"12."); + CHECK(Ime::HostCancel(snapshot.generation)); + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(Ime::ImeDialogTerm() == 0); +} + +void TestExternalKeyboardFilter() { + std::array text{}; + auto param = MakeParam(text.data()); + Ime::ExtendedParam extended{}; + extended.ext_keyboard_filter = KeyboardFilter; + CHECK(Ime::ImeDialogInit(¶m, &extended) == 0); + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + + Ime::ExternalInput input{}; + input.key.status = 3; + input.key.keycode = 4; + input.action = Ime::ExternalAction::Text; + input.text = u"x"; + g_keyboard_filter_calls = 0; + g_block_keyboard = true; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, input)); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_keyboard_filter_calls == 1); + CHECK(text[0] == u'\0'); + + g_block_keyboard = false; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_keyboard_filter_calls == 2); + CHECK(std::u16string(text.data()) == u"x"); + + input = {}; + input.key.status = 3; + input.key.keycode = 4; + input.action = Ime::ExternalAction::Text; + input.text = u"x"; + g_remap_keyboard = 5; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_keyboard_filter_calls == 3); + CHECK(std::u16string(text.data()) == u"xb"); + + input = {}; + input.key.status = 0x00000201; + input.key.keycode = 4; + input.action = Ime::ExternalAction::Text; + input.text = u"x"; + g_remap_keyboard = 50; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(g_keyboard_filter_calls == 4); + CHECK(std::u16string(text.data()) == u"xb~"); + + input = {}; + input.key.status = 3; + input.key.keycode = 4; + input.action = Ime::ExternalAction::Text; + input.text = u"x"; + g_remap_keyboard = 88; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Finished)); + CHECK(g_keyboard_filter_calls == 5); + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(result.endstatus == Ime::EndStatus::Ok); + CHECK(Ime::ImeDialogTerm() == 0); + + g_remap_keyboard = 0; + CHECK(Ime::ImeDialogInit(¶m, &extended) == 0); + CHECK(Ime::GetHostSnapshot(&snapshot)); + + input = {}; + input.key.status = 1; + input.key.keycode = 41; + input.action = Ime::ExternalAction::Cancel; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + input = {}; + input.key.status = 3; + input.key.keycode = 4; + input.action = Ime::ExternalAction::Text; + input.text = u"ignored"; + CHECK(Ime::HostQueueExternalInput(snapshot.generation, std::move(input))); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Finished)); + CHECK(g_keyboard_filter_calls == 6); + result = {}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(Ime::ImeDialogTerm() == 0); +} + +void TestFilteredCursorBoundary() { + std::array text{}; + auto param = MakeParam(text.data()); + param.filter = SupplementaryFilter; + CHECK(Ime::ImeDialogInit(¶m, nullptr) == 0); + Ime::HostSnapshot snapshot; + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(Ime::HostInsertText(snapshot.generation, u"x")); + CHECK(Ime::ImeDialogGetStatus() == static_cast(Ime::Status::Running)); + CHECK(Ime::GetHostSnapshot(&snapshot)); + CHECK(snapshot.cursor == 2); + CHECK(text[0] == 0xd83d && text[1] == 0xde00 && text[2] == 0); + CHECK(Ime::HostCancel(snapshot.generation)); + Ime::Result result{}; + CHECK(Ime::ImeDialogGetResult(&result) == 0); + CHECK(Ime::ImeDialogTerm() == 0); +} + +} // namespace + +int main() { + TestLayoutAndPanelSize(); + TestAcceptLifecycle(); + TestCancelAndAbortRestoreText(); + TestValidation(); + TestFilteringAndInputPolicy(); + TestExternalKeyboardFilter(); + TestFilteredCursorBoundary(); + return 0; +}