diff --git a/src/graphics/presentation/imeDialogOverlay.h b/src/graphics/presentation/imeDialogOverlay.h deleted file mode 100644 index 7aeafee..0000000 --- a/src/graphics/presentation/imeDialogOverlay.h +++ /dev/null @@ -1,36 +0,0 @@ -#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/imeDialogOverlay.cpp b/src/graphics/presentation/imeOverlay.cpp similarity index 78% rename from src/graphics/presentation/imeDialogOverlay.cpp rename to src/graphics/presentation/imeOverlay.cpp index e6f405a..3c95faf 100644 --- a/src/graphics/presentation/imeDialogOverlay.cpp +++ b/src/graphics/presentation/imeOverlay.cpp @@ -1,4 +1,4 @@ -#include "graphics/presentation/imeDialogOverlay.h" +#include "graphics/presentation/imeOverlay.h" #include "SDL.h" #include "common/assert.h" @@ -7,6 +7,7 @@ #include "imgui.h" #include "imgui_impl_vulkan.h" #include "libs/controller.h" +#include "libs/ime.h" #include "libs/imeDialog.h" #include @@ -26,7 +27,181 @@ namespace Libs::Graphics { namespace { -namespace Ime = Libs::Dialog::ImeDialog; +namespace CoreIme = Libs::Ime; +namespace DialogIme = Libs::Dialog::ImeDialog; + +namespace Ime { + +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 class ExternalAction : uint8_t { + None, + Text, + Backspace, + MoveLeft, + MoveRight, + Cancel, + Accept, + Newline, +}; + +struct Keycode { + uint16_t keycode = 0; + char16_t character = u'\0'; + uint32_t status = 0; + uint32_t type = 0; + int32_t user_id = 0; + uint32_t resource_id = 0; + uint64_t timestamp = 0; +}; + +struct ExternalInput { + Keycode key; + ExternalAction action = ExternalAction::None; + std::u16string text; +}; + +struct HostSnapshot { + uint64_t generation = 0; + Type type = Type::Default; + EnterLabel enter_label = EnterLabel::Default; + uint32_t option = 0; + uint32_t max_text_length = 0; + uint32_t cursor = 0; + uint32_t disable_device = 0; + bool key_panel_visible = true; + float posx = 0.0f; + float posy = 0.0f; + Alignment horizontal_alignment = Alignment::Start; + Alignment vertical_alignment = Alignment::Start; + uint32_t panel_width = 0; + uint32_t panel_height = 0; + std::u16string text; + std::u16string title; + std::u16string placeholder; +}; + +constexpr uint32_t OPTION_MULTILINE = 0x00000001; +constexpr uint32_t OPTION_NO_AUTO_CAPITALIZE = 0x00000002; +constexpr uint32_t OPTION_PASSWORD = 0x00000004; +constexpr uint32_t OPTION_FIXED_POSITION = 0x00000040; +constexpr uint32_t OPTION_DISABLE_POSITION_ADJ = 0x00000800; +constexpr uint32_t OPTION_USE_OVER_2K = 0x00004000; +constexpr uint32_t DISABLE_DEVICE_CONTROLLER = 0x00000001; +constexpr uint32_t DISABLE_DEVICE_EXT_KEYBOARD = 0x00000002; +constexpr uint64_t CORE_GENERATION_BIT = uint64_t {1} << 63; + +uint64_t PackCoreGeneration(uint64_t generation) { + return generation | CORE_GENERATION_BIT; +} + +bool IsCoreGeneration(uint64_t generation) { + return (generation & CORE_GENERATION_BIT) != 0; +} + +uint64_t UnpackGeneration(uint64_t generation) { + return generation & ~CORE_GENERATION_BIT; +} + +bool GetHostSnapshot(HostSnapshot* snapshot) { + if (snapshot == nullptr) { + return false; + } + CoreIme::HostSnapshot core; + if (CoreIme::GetHostSnapshot(&core)) { + snapshot->generation = PackCoreGeneration(core.generation); + snapshot->type = static_cast(core.type); + snapshot->enter_label = static_cast(core.enter_label); + snapshot->option = core.option; + snapshot->max_text_length = core.max_text_length; + snapshot->cursor = core.cursor; + snapshot->disable_device = core.disable_device; + snapshot->key_panel_visible = core.key_panel_visible; + snapshot->posx = core.posx; + snapshot->posy = core.posy; + snapshot->horizontal_alignment = static_cast(core.horizontal_alignment); + snapshot->vertical_alignment = static_cast(core.vertical_alignment); + snapshot->panel_width = core.panel_width; + snapshot->panel_height = core.panel_height; + snapshot->text = std::move(core.text); + snapshot->title.clear(); + snapshot->placeholder.clear(); + return true; + } + DialogIme::HostSnapshot dialog; + if (!DialogIme::GetHostSnapshot(&dialog)) { + return false; + } + snapshot->generation = dialog.generation; + snapshot->type = static_cast(dialog.type); + snapshot->enter_label = static_cast(dialog.enter_label); + snapshot->option = dialog.option; + snapshot->max_text_length = dialog.max_text_length; + snapshot->cursor = dialog.cursor; + snapshot->disable_device = dialog.disable_device; + snapshot->key_panel_visible = dialog.key_panel_visible; + snapshot->posx = dialog.posx; + snapshot->posy = dialog.posy; + snapshot->horizontal_alignment = static_cast(dialog.horizontal_alignment); + snapshot->vertical_alignment = static_cast(dialog.vertical_alignment); + snapshot->panel_width = dialog.panel_width; + snapshot->panel_height = dialog.panel_height; + snapshot->text = std::move(dialog.text); + snapshot->title = std::move(dialog.title); + snapshot->placeholder = std::move(dialog.placeholder); + return true; +} + +bool HostInsertText(uint64_t generation, std::u16string_view text) { + return IsCoreGeneration(generation) + ? CoreIme::HostInsertText(UnpackGeneration(generation), text) + : DialogIme::HostInsertText(generation, text); +} + +bool HostBackspace(uint64_t generation) { + return IsCoreGeneration(generation) ? CoreIme::HostBackspace(UnpackGeneration(generation)) + : DialogIme::HostBackspace(generation); +} + +bool HostAccept(uint64_t generation) { + return IsCoreGeneration(generation) ? CoreIme::HostAccept(UnpackGeneration(generation)) + : DialogIme::HostAccept(generation); +} + +bool HostCancel(uint64_t generation) { + return IsCoreGeneration(generation) ? CoreIme::HostCancel(UnpackGeneration(generation)) + : DialogIme::HostCancel(generation); +} + +bool HostQueueExternalInput(uint64_t generation, ExternalInput input) { + if (!IsCoreGeneration(generation)) { + DialogIme::ExternalInput dialog; + dialog.key.keycode = input.key.keycode; + dialog.key.character = input.key.character; + dialog.key.status = input.key.status; + dialog.key.type = input.key.type; + dialog.key.user_id = input.key.user_id; + dialog.key.resource_id = input.key.resource_id; + dialog.key.timestamp = input.key.timestamp; + dialog.action = static_cast(input.action); + dialog.text = std::move(input.text); + return DialogIme::HostQueueExternalInput(generation, std::move(dialog)); + } + CoreIme::ExternalInput core; + core.key.keycode = input.key.keycode; + core.key.character = input.key.character; + core.key.status = input.key.status; + core.key.type = input.key.type; + core.key.user_id = input.key.user_id; + core.key.resource_id = input.key.resource_id; + core.key.timestamp = input.key.timestamp; + core.action = static_cast(input.action); + core.text = std::move(input.text); + return CoreIme::HostQueueExternalInput(UnpackGeneration(generation), std::move(core)); +} + +} // namespace Ime constexpr size_t INPUT_QUEUE_CAPACITY = 128; @@ -114,28 +289,25 @@ void RetryVisibilityWakeup() { } } -void OnVisibilityChanged(bool visible, uint64_t generation) { +void RefreshVisibility() { std::scoped_lock visibility_lock(g_visibility_mutex); if (!g_input_lifecycle_active) { return; } - if (generation < g_generation || (generation == g_generation && visible == g_active)) { + Ime::HostSnapshot snapshot; + const bool visible = Ime::GetHostSnapshot(&snapshot); + const uint64_t generation = visible ? snapshot.generation : g_generation; + if (visible == g_active && (!visible || generation == g_generation)) { 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; - } + 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; } const bool was_controller = std::exchange(g_controller_captured, capture_controller); if (capture_controller || was_controller) { @@ -156,6 +328,14 @@ void OnVisibilityChanged(bool visible, uint64_t generation) { } } +void OnCoreVisibilityChanged(bool, uint64_t) { + RefreshVisibility(); +} + +void OnDialogVisibilityChanged(bool, uint64_t) { + RefreshVisibility(); +} + uint32_t ExternalKeyStatus(SDL_Keymod modifiers, bool character_valid) { uint32_t status = 0x00000001 | (character_valid ? 0x00000002 : 0); if ((modifiers & KMOD_LCTRL) != 0) status |= 0x00000100; @@ -305,7 +485,7 @@ void CheckVulkanResult(VkResult result) { } // namespace -void InitializeImeDialogInput() { +void InitializeImeInput() { const Uint32 type = SDL_RegisterEvents(1); EXIT_IF(type == static_cast(-1)); { @@ -313,15 +493,14 @@ void InitializeImeDialogInput() { 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); - } + CoreIme::SetVisibilityCallback(OnCoreVisibilityChanged); + DialogIme::SetVisibilityCallback(OnDialogVisibilityChanged); + RefreshVisibility(); } -void ShutdownImeDialogInput() { - Ime::SetVisibilityCallback(nullptr); +void ShutdownImeInput() { + CoreIme::SetVisibilityCallback(nullptr); + DialogIme::SetVisibilityCallback(nullptr); { std::scoped_lock lock(g_visibility_mutex); g_input_lifecycle_active = false; @@ -346,7 +525,13 @@ void ShutdownImeDialogInput() { } } -bool ProcessImeDialogInput(const SDL_Event& event) { +ImeVisualState GetImeVisualState() noexcept { + const auto core = CoreIme::GetVisualState(); + const auto dialog = DialogIme::GetVisualState(); + return {core.active || dialog.active, core.revision + dialog.revision}; +} + +bool ProcessImeInput(const SDL_Event& event) { RetryVisibilityWakeup(); if (event.type == g_visibility_event.load(std::memory_order_acquire)) { VisibilityUpdate update {}; @@ -465,7 +650,7 @@ bool ProcessImeDialogInput(const SDL_Event& event) { } } -struct ImeDialogOverlay::Impl { +struct ImeOverlay::Impl { explicit Impl(GraphicContext& context): graphics(context) {} ~Impl() { @@ -488,7 +673,7 @@ struct ImeDialogOverlay::Impl { io.LogFilename = nullptr; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; io.BackendFlags |= ImGuiBackendFlags_HasGamepad; - io.BackendPlatformName = "Kyty ImeDialog input"; + io.BackendPlatformName = "Kyty IME input"; ImGui::StyleColorsDark(); auto& style = ImGui::GetStyle(); style.WindowRounding = 10.0f; @@ -680,7 +865,7 @@ struct ImeDialogOverlay::Impl { ImGui::SetNextWindowSize({width, height}, ImGuiCond_Always); constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings; - ImGui::Begin("##ImeDialog", nullptr, flags); + ImGui::Begin("##Ime", nullptr, flags); const std::u16string title = snapshot.title.empty() ? u"Enter text" : snapshot.title; if (!snapshot.key_panel_visible) { @@ -856,20 +1041,19 @@ struct ImeDialogOverlay::Impl { std::chrono::steady_clock::time_point last_frame; }; -ImeDialogOverlay::ImeDialogOverlay(GraphicContext& graphics) - : m_impl(std::make_unique(graphics)) {} +ImeOverlay::ImeOverlay(GraphicContext& graphics): m_impl(std::make_unique(graphics)) {} -ImeDialogOverlay::~ImeDialogOverlay() = default; +ImeOverlay::~ImeOverlay() = default; -bool ImeDialogOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) { +bool ImeOverlay::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) { +void ImeOverlay::Record(vk::CommandBuffer command, vk::ImageView target) { m_impl->Record(command, target); } -void ImeDialogOverlay::ReleaseVulkan() { +void ImeOverlay::ReleaseVulkan() { m_impl->ReleaseVulkan(); } diff --git a/src/graphics/presentation/imeOverlay.h b/src/graphics/presentation/imeOverlay.h new file mode 100644 index 0000000..6ba71c8 --- /dev/null +++ b/src/graphics/presentation/imeOverlay.h @@ -0,0 +1,42 @@ +#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEOVERLAY_H_ +#define EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEOVERLAY_H_ + +#include "common/common.h" +#include "graphics/host_gpu/vulkanCommon.h" + +#include + +union SDL_Event; + +namespace Libs::Graphics { + +struct GraphicContext; + +struct ImeVisualState { + bool active; + uint64_t revision; +}; + +void InitializeImeInput(); +void ShutdownImeInput(); +bool ProcessImeInput(const SDL_Event& event); +ImeVisualState GetImeVisualState() noexcept; + +class ImeOverlay final { +public: + explicit ImeOverlay(GraphicContext& graphics); + ~ImeOverlay(); + KYTY_CLASS_NO_COPY(ImeOverlay); + + [[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_IMEOVERLAY_H_ diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index 84645b4..931030d 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -31,13 +31,12 @@ #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/imeOverlay.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 @@ -345,17 +344,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; - std::unique_ptr m_ime_overlay; - 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 { @@ -618,7 +617,7 @@ Swapchain::Status Swapchain::AcquireNextImage() { bool Swapchain::PrepareImeOverlay() { if (m_ime_overlay == nullptr) { - m_ime_overlay = std::make_unique(m_window.graphic_ctx); + m_ime_overlay = std::make_unique(m_window.graphic_ctx); } return m_ime_overlay->PrepareFrame(m_extent, m_format, ImageCount()); } @@ -807,7 +806,7 @@ bool Presenter::IsGuestPaused() const noexcept { } bool Presenter::NeedsImeRefresh() const noexcept { - const auto visual = Dialog::ImeDialog::GetVisualState(); + const auto visual = GetImeVisualState(); return visual.active || visual.revision != m_impl->presented_ime_revision.load(std::memory_order_acquire); } @@ -843,7 +842,7 @@ void Presenter::Present(Frame& frame, bool reuse) { m_impl->RecoverSwapchain(Swapchain::Status::Recreate); } - const auto ime_visual = Dialog::ImeDialog::GetVisualState(); + const auto ime_visual = GetImeVisualState(); auto& swapchain = m_impl->swapchain; for (uint32_t attempt = 0; attempt < 2; attempt++) { auto status = swapchain.AcquireNextImage(); diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index ceb807a..6b93f29 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -31,7 +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/imeOverlay.h" #include "graphics/presentation/presenter.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/videoOut.h" @@ -1015,7 +1015,7 @@ void WindowContext::RecreateSurface() { } WindowContext::~WindowContext() { - ShutdownImeDialogInput(); + ShutdownImeInput(); 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 a6332b1..a5d962f 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -31,7 +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/imeOverlay.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/window/hostInput.h" #include "graphics/presentation/window/windowInternal.h" @@ -477,7 +477,7 @@ void WindowContext::ProcessEvent(double time_s) { auto& game = loop; auto* event = &game.event; EXIT_IF(SDL_GetEventState(SDL_DISPLAYEVENT) != SDL_ENABLE); - if (ProcessImeDialogInput(*event)) { + if (ProcessImeInput(*event)) { return; } @@ -783,7 +783,7 @@ static void WindowCreate(WindowContext& context) { EXIT("%s\n", SDL_GetError()); } HostInputInit(); - InitializeImeDialogInput(); + InitializeImeInput(); LOGF("WindowCreate(): width = %d, height = %d\n", width, height); @@ -938,7 +938,6 @@ void WindowContext::UpdateTitle() { static constexpr auto build_type = "Unknown"; #endif - const auto now = Common::Timer::QueryPerformanceCounter(); const auto frequency = Common::Timer::QueryPerformanceFrequency(); frame_num++; @@ -950,11 +949,11 @@ void WindowContext::UpdateTitle() { fps_frames = 0; } - auto fps = fmt::format("[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", KYTY_BUILD_LABEL, - build_type, (has_title ? title : ""), (has_title ? ", " : ""), - (has_title_id ? title_id : ""), (has_title_id ? ", " : ""), - (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""), device_name, - processor_name, frame_num, current_fps); + auto fps = fmt::format( + "[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", KYTY_BUILD_LABEL, build_type, + (has_title ? title : ""), (has_title ? ", " : ""), (has_title_id ? title_id : ""), + (has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""), + device_name, processor_name, frame_num, current_fps); #if defined(__APPLE__) // AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing. diff --git a/src/libs/ime.cpp b/src/libs/ime.cpp new file mode 100644 index 0000000..d843015 --- /dev/null +++ b/src/libs/ime.cpp @@ -0,0 +1,1020 @@ +#include "libs/ime.h" + +#include "libs/errno.h" +#include "libs/libs.h" +#include "loader/symbolDatabase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Ime { +namespace { + +constexpr int Error(uint32_t value) { + return static_cast(value); +} + +constexpr int ERROR_BUSY = Error(0x80bc0001); +constexpr int ERROR_NOT_OPENED = Error(0x80bc0002); +constexpr int ERROR_CONNECTION_FAILED = Error(0x80bc0004); +constexpr int ERROR_EVENT_OVERFLOW = Error(0x80bc0007); +constexpr int ERROR_INVALID_TEXT = Error(0x80bc0006); +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_WORK = Error(0x80bc0020); +constexpr int ERROR_INVALID_HANDLER = Error(0x80bc0022); +constexpr int ERROR_NO_RESOURCE_ID = Error(0x80bc0023); +constexpr int ERROR_INVALID_MODE = Error(0x80bc0024); +constexpr int ERROR_INVALID_PARAM = Error(0x80bc0030); +constexpr int ERROR_INVALID_ADDRESS = Error(0x80bc0031); +constexpr int ERROR_INVALID_RESERVED = Error(0x80bc0032); + +constexpr uint32_t VALID_OPTIONS = 0x00007bff; +constexpr uint32_t VALID_EXTENDED_OPTIONS = 0x00004fde; +constexpr uint32_t VALID_EXT_KEYBOARD_MODE = 0x1c000003; +constexpr uint32_t VALID_KEYBOARD_OPTIONS = 0x0000003f; +constexpr uint32_t VALID_KEYBOARD_MODE = 0x0000007f; +constexpr uint64_t VALID_LANGUAGES = 0x00000001ff1fffffULL; + +constexpr uint32_t OPTION_MULTILINE = 0x00000001; +constexpr uint32_t OPTION_PASSWORD = 0x00000004; +constexpr uint32_t OPTION_EXT_KEYBOARD = 0x00000010; +constexpr uint32_t OPTION_EXPANDED_PREEDIT = 0x00001000; +constexpr uint32_t OPTION_USE_OVER_2K = 0x00004000; + +constexpr uint32_t EXT_OPTION_HIDE_KEY_PANEL = 0x00000400; +constexpr size_t EVENT_QUEUE_CAPACITY = 128; + +struct QueuedEvent { + Event event {}; + bool text_payload = false; + std::u16string text; +}; + +struct State { + bool open = false; + bool event_overflow = false; + uint64_t generation = 0; + uint64_t revision = 0; + uint32_t option = 0; + uint32_t max_text_length = 0; + uint32_t cursor = 0; + Param param {}; + ExtendedParam extended {}; + void* arg = nullptr; + EventHandler handler = nullptr; + std::u16string text; + std::deque events; + std::vector external_inputs; +}; + +struct KeyboardState { + bool open = false; + void* arg = nullptr; + EventHandler handler = nullptr; + std::deque events; +}; + +std::mutex g_mutex; +State g_state; +KeyboardState g_keyboard_state; +std::atomic g_open {false}; +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 ValidateText(const char16_t* text, uint32_t length, bool multiline, uint32_t* actual_length) { + if (text == nullptr) { + return false; + } + *actual_length = 0; + for (uint32_t i = 0; i < length; i++) { + const char16_t value = text[i]; + if (value == u'\0') { + return true; + } + if (!multiline && (value == u'\n' || value == u'\r')) { + return false; + } + if (value >= 0xd800 && value <= 0xdbff) { + if (++i >= length || text[i] < 0xdc00 || text[i] > 0xdfff) { + return false; + } + } else if (value >= 0xdc00 && value <= 0xdfff) { + return false; + } + *actual_length = i + 1; + } + return true; +} + +bool IsAllowedInput(char16_t value, Type type, uint32_t option) { + if (value == u'\r' || value == u'\0') { + return false; + } + if (value == u'\n') { + return (option & OPTION_MULTILINE) != 0; + } + 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) { + return static_cast(((shift != caps) ? 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]; + } + return keycode == 99 ? u'.' : 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; +} + +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 NotifyVisibility(bool visible, uint64_t generation) { + if (const auto callback = g_visibility_callback.load(std::memory_order_acquire); + callback != nullptr) { + callback(visible, generation); + } +} + +void UpdateRevisionLocked() { + g_state.revision++; + g_revision.store(g_state.revision, std::memory_order_release); +} + +void SyncGuestText(char16_t* work, char16_t* input, uint32_t max_length, std::u16string_view text) { + const size_t size = std::min(text.size(), max_length); + if (work != nullptr) { + std::memcpy(work, text.data(), size * sizeof(char16_t)); + work[size] = u'\0'; + } + if (input != nullptr) { + std::memcpy(input, text.data(), size * sizeof(char16_t)); + input[size] = u'\0'; + } +} + +void SyncTextBuffersLocked() { + SyncGuestText(static_cast(g_state.param.work), g_state.param.input_text_buffer, + g_state.max_text_length, g_state.text); +} + +bool QueueEventLocked(QueuedEvent event) { + if (g_state.events.size() == EVENT_QUEUE_CAPACITY) { + g_state.event_overflow = true; + return false; + } + g_state.events.push_back(event); + return true; +} + +QueuedEvent MakeTextEventLocked(uint32_t id, uint32_t edit_index, int32_t edit_length) { + QueuedEvent queued; + queued.event.id = id; + queued.event.param.text.str = static_cast(g_state.param.work); + queued.event.param.text.caret_index = g_state.cursor; + queued.event.param.text.area_num = 1; + queued.event.param.text.text_area[0] = {TextAreaMode::Edit, edit_index, edit_length}; + queued.text_payload = true; + queued.text = g_state.text; + return queued; +} + +bool CommitFilteredText(uint64_t generation, uint64_t revision, std::u16string old_text, + std::u16string candidate, uint32_t cursor, TextFilter filter, + uint32_t max_length, bool multiline) { + if (filter != nullptr) { + std::vector output(max_length + 1, u'\0'); + uint32_t output_length = max_length; + if (filter(output.data(), &output_length, candidate.c_str(), + static_cast(candidate.size())) == 0 && + output_length <= max_length) { + uint32_t actual_length = 0; + if (ValidateText(output.data(), output_length, multiline, &actual_length) && + actual_length == output_length) { + candidate.assign(output.data(), output.data() + output_length); + } + } + } + + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation || g_state.revision != revision || + g_state.text != old_text || candidate == old_text) { + return false; + } + size_t prefix = 0; + while (prefix < old_text.size() && prefix < candidate.size() && + old_text[prefix] == candidate[prefix]) { + prefix++; + } + size_t old_tail = old_text.size(); + size_t new_tail = candidate.size(); + while (old_tail > prefix && new_tail > prefix && + old_text[old_tail - 1] == candidate[new_tail - 1]) { + old_tail--; + new_tail--; + } + const int32_t edit_length = + static_cast(new_tail - prefix) - static_cast(old_tail - prefix); + g_state.text = std::move(candidate); + g_state.cursor = NormalizeCursor(g_state.text, cursor); + QueueEventLocked(MakeTextEventLocked(1, static_cast(prefix), edit_length)); + UpdateRevisionLocked(); + return true; +} + +void ApplyExternalInputs(); + +bool ReadInitialText(const Param& param, std::u16string* text) { + uint32_t length = 0; + while (length <= param.max_text_length && param.input_text_buffer[length] != u'\0') { + length++; + } + if (length > param.max_text_length) { + return false; + } + uint32_t actual = 0; + if (!ValidateText(param.input_text_buffer, length, (param.option & OPTION_MULTILINE) != 0, + &actual)) { + return false; + } + text->assign(param.input_text_buffer, param.input_text_buffer + actual); + return true; +} + +bool RangesOverlap(uintptr_t first, size_t first_size, uintptr_t second, size_t second_size) { + if (first > UINTPTR_MAX - first_size || second > UINTPTR_MAX - second_size) { + return true; + } + return first < second + second_size && second < first + first_size; +} + +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 ValidateOpen(const Param* param, const ExtendedParam* extended, std::u16string* text) { + if (param == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (param->user_id < 0 || param->user_id == 0xff) { + return ERROR_INVALID_USER_ID; + } + if (static_cast(param->type) > static_cast(Type::Number)) { + return ERROR_INVALID_TYPE; + } + if ((param->supported_languages & ~VALID_LANGUAGES) != 0) { + return ERROR_INVALID_LANGUAGES; + } + 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->option & ~VALID_OPTIONS) != 0) { + return ERROR_INVALID_OPTION; + } + 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->max_text_length == 0 || param->max_text_length > MAX_TEXT_LENGTH) { + return ERROR_INVALID_MAX_TEXT_LENGTH; + } + if (param->input_text_buffer == nullptr) { + return ERROR_INVALID_TEXT_BUFFER; + } + 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 int extended_result = ValidateExtended(extended); + if (extended_result != OK) { + return extended_result; + } + if (param->work == nullptr || (reinterpret_cast(param->work) & 3u) != 0) { + return ERROR_INVALID_WORK; + } + if (param->handler == nullptr) { + return ERROR_INVALID_HANDLER; + } + if (!AllZero(param->reserved, sizeof(param->reserved))) { + return ERROR_INVALID_RESERVED; + } + const size_t text_size = + static_cast(param->max_text_length + + ((param->option & OPTION_EXPANDED_PREEDIT) != 0 ? 0x79u : 0x1fu)) * + sizeof(char16_t); + if (RangesOverlap(reinterpret_cast(param->input_text_buffer), text_size, + reinterpret_cast(param->work), WORK_BUFFER_SIZE)) { + return ERROR_INVALID_PARAM; + } + return ReadInitialText(*param, text) ? OK : ERROR_INVALID_TEXT; +} + +} // namespace + +void KYTY_SYSV_ABI ImeParamInit(Param* param) { + if (param == nullptr) { + return; + } + std::memset(param, 0, sizeof(*param)); + param->user_id = -1; +} + +int KYTY_SYSV_ABI ImeGetPanelSize(const Param* param, 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; + } + *width = param->type == Type::Number ? 370 : 793; + *height = param->type == Type::Number ? 402 : 408; + if ((param->option & OPTION_USE_OVER_2K) != 0) { + *width *= 2; + *height *= 2; + } + return OK; +} + +int KYTY_SYSV_ABI ImeOpen(const Param* param, const ExtendedParam* extended) { + uint64_t generation = 0; + { + std::scoped_lock lock(g_mutex); + if (g_state.open) { + return ERROR_BUSY; + } + std::u16string text; + const int validation = ValidateOpen(param, extended, &text); + if (validation != OK) { + return validation; + } + const uint64_t next_generation = g_state.generation + 1; + const uint64_t next_revision = g_state.revision + 1; + g_state = {}; + g_state.open = true; + g_state.generation = next_generation; + g_state.revision = next_revision; + g_state.option = param->option; + g_state.max_text_length = param->max_text_length; + g_state.cursor = static_cast(text.size()); + g_state.param = *param; + if (extended != nullptr) { + g_state.extended = *extended; + } + g_state.arg = param->arg; + g_state.handler = param->handler; + g_state.text = std::move(text); + std::memset(param->work, 0, WORK_BUFFER_SIZE); + SyncTextBuffersLocked(); + + QueuedEvent open; + open.event.id = 0; + open.event.param.rect.x = param->posx; + open.event.param.rect.y = param->posy; + open.event.param.rect.width = param->type == Type::Number ? 370 : 793; + open.event.param.rect.height = param->type == Type::Number ? 402 : 408; + if ((param->option & OPTION_USE_OVER_2K) != 0) { + open.event.param.rect.width *= 2; + open.event.param.rect.height *= 2; + } + QueueEventLocked(std::move(open)); + generation = g_state.generation; + g_open.store(true, std::memory_order_release); + g_revision.store(g_state.revision, std::memory_order_release); + } + NotifyVisibility(true, generation); + return OK; +} + +int KYTY_SYSV_ABI ImeUpdate(EventHandler handler) { + bool apply_inputs = false; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open && !g_keyboard_state.open) { + return ERROR_NOT_OPENED; + } + const bool update_ime = g_state.open && handler == g_state.handler; + const bool update_keyboard = g_keyboard_state.open && handler == g_keyboard_state.handler; + if (!update_ime && !update_keyboard) { + return ERROR_NOT_OPENED; + } + apply_inputs = update_ime; + } + if (apply_inputs) { + ApplyExternalInputs(); + } + struct Dispatch { + void* arg; + QueuedEvent queued; + char16_t* work; + char16_t* input; + uint32_t max_length; + uint64_t generation; + }; + std::vector dispatch; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open && !g_keyboard_state.open) { + return ERROR_NOT_OPENED; + } + const bool update_ime = g_state.open && handler == g_state.handler; + const bool update_keyboard = g_keyboard_state.open && handler == g_keyboard_state.handler; + if (!update_ime && !update_keyboard) { + return ERROR_NOT_OPENED; + } + if (update_ime && std::exchange(g_state.event_overflow, false)) { + return ERROR_EVENT_OVERFLOW; + } + if (update_ime) { + while (!g_state.events.empty()) { + dispatch.push_back({g_state.arg, std::move(g_state.events.front()), + static_cast(g_state.param.work), + g_state.param.input_text_buffer, g_state.max_text_length, + g_state.generation}); + g_state.events.pop_front(); + } + } + if (update_keyboard) { + while (!g_keyboard_state.events.empty()) { + dispatch.push_back({g_keyboard_state.arg, + std::move(g_keyboard_state.events.front()), nullptr, nullptr, 0, + 0}); + g_keyboard_state.events.pop_front(); + } + } + } + for (auto& item: dispatch) { + if (item.generation != 0) { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != item.generation) { + continue; + } + } + if (item.queued.text_payload) { + SyncGuestText(item.work, item.input, item.max_length, item.queued.text); + item.queued.event.param.text.str = item.work; + } + handler(item.arg, &item.queued.event); + } + return OK; +} + +int KYTY_SYSV_ABI ImeClose() { + uint64_t generation = 0; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open) { + return ERROR_NOT_OPENED; + } + generation = g_state.generation; + const uint64_t revision = g_state.revision + 1; + g_state = {}; + g_state.generation = generation; + g_state.revision = revision; + g_open.store(false, std::memory_order_release); + g_revision.store(revision, std::memory_order_release); + } + NotifyVisibility(false, generation); + return OK; +} + +int KYTY_SYSV_ABI ImeSetText(const char16_t* text, uint32_t length) { + std::scoped_lock lock(g_mutex); + if (!g_state.open) { + return ERROR_NOT_OPENED; + } + if (text == nullptr) { + return ERROR_INVALID_ADDRESS; + } + const uint32_t clamped_length = std::min(length, g_state.max_text_length); + uint32_t actual_length = 0; + if (!ValidateText(text, clamped_length, (g_state.option & OPTION_MULTILINE) != 0, + &actual_length)) { + return ERROR_INVALID_TEXT; + } + g_state.text.assign(text, text + actual_length); + g_state.cursor = NormalizeCursor(g_state.text, g_state.cursor); + SyncTextBuffersLocked(); + UpdateRevisionLocked(); + return OK; +} + +int KYTY_SYSV_ABI ImeSetCaret(const Caret* caret) { + std::scoped_lock lock(g_mutex); + if (!g_state.open) { + return ERROR_NOT_OPENED; + } + if (caret == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (caret->index > g_state.text.size()) { + return ERROR_INVALID_PARAM; + } + g_state.cursor = NormalizeCursor(g_state.text, caret->index); + UpdateRevisionLocked(); + return OK; +} + +int KYTY_SYSV_ABI ImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry) { + std::scoped_lock lock(g_mutex); + if (!g_state.open) { + return ERROR_NOT_OPENED; + } + if (geometry == nullptr) { + return ERROR_INVALID_ADDRESS; + } + const bool over_2k = (g_state.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(geometry->x) || !std::isfinite(geometry->y) || geometry->x < 0.0f || + geometry->x >= max_x || geometry->y < 0.0f || geometry->y >= max_y || + (mode != TextAreaMode::Preedit && mode != TextAreaMode::Select)) { + return ERROR_INVALID_PARAM; + } + return OK; +} + +int KYTY_SYSV_ABI ImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { + std::scoped_lock lock(g_mutex); + if (param == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (user_id == -1) { + return ERROR_INVALID_USER_ID; + } + if (param->handler == nullptr) { + return ERROR_INVALID_HANDLER; + } + if ((param->option & ~VALID_KEYBOARD_OPTIONS) != 0) { + return ERROR_INVALID_OPTION; + } + if (!AllZero(param->reserved1, sizeof(param->reserved1)) || + !AllZero(param->reserved2, sizeof(param->reserved2))) { + return ERROR_INVALID_RESERVED; + } + if (g_keyboard_state.open) { + return ERROR_BUSY; + } + g_keyboard_state = {}; + g_keyboard_state.open = true; + g_keyboard_state.arg = param->arg; + g_keyboard_state.handler = param->handler; + QueuedEvent open; + open.event.id = 256; + open.event.param.resource_id_array.user_id = user_id; + g_keyboard_state.events.push_back(std::move(open)); + return OK; +} + +int KYTY_SYSV_ABI ImeKeyboardClose(int32_t user_id) { + std::scoped_lock lock(g_mutex); + if (!g_keyboard_state.open) { + return ERROR_NOT_OPENED; + } + if (user_id == -1) { + return ERROR_INVALID_USER_ID; + } + g_keyboard_state = {}; + return OK; +} + +int KYTY_SYSV_ABI ImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids) { + if (resource_ids == nullptr) { + return ERROR_INVALID_ADDRESS; + } + if (user_id == -1) { + return ERROR_INVALID_USER_ID; + } + std::memset(resource_ids, 0, sizeof(*resource_ids)); + resource_ids->user_id = user_id; + std::scoped_lock lock(g_mutex); + return g_keyboard_state.open ? ERROR_CONNECTION_FAILED : ERROR_NOT_OPENED; +} + +int KYTY_SYSV_ABI ImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info) { + if (info == nullptr) { + return ERROR_INVALID_ADDRESS; + } + std::scoped_lock lock(g_mutex); + if (!g_keyboard_state.open) { + return ERROR_NOT_OPENED; + } + (void)resource_id; + return ERROR_NO_RESOURCE_ID; +} + +int KYTY_SYSV_ABI ImeKeyboardSetMode(int32_t user_id, uint32_t mode) { + if (user_id == -1) { + return ERROR_INVALID_USER_ID; + } + std::scoped_lock lock(g_mutex); + if (!g_keyboard_state.open) { + return ERROR_NOT_OPENED; + } + return (mode & ~VALID_KEYBOARD_MODE) == 0 ? OK : ERROR_INVALID_MODE; +} + +VisualState GetVisualState() noexcept { + return {g_open.load(std::memory_order_acquire), 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.open) { + 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.option; + snapshot->max_text_length = g_state.max_text_length; + snapshot->cursor = g_state.cursor; + snapshot->disable_device = g_state.extended.disable_device; + snapshot->key_panel_visible = (g_state.option & OPTION_EXT_KEYBOARD) == 0 || + (g_state.extended.option & EXT_OPTION_HIDE_KEY_PANEL) == 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; + snapshot->panel_width = g_state.param.type == Type::Number ? 370 : 793; + snapshot->panel_height = g_state.param.type == Type::Number ? 402 : 408; + if ((g_state.option & OPTION_USE_OVER_2K) != 0) { + snapshot->panel_width *= 2; + snapshot->panel_height *= 2; + } + snapshot->text = g_state.text; + return true; +} + +static bool ApplyInsertText(uint64_t generation, std::u16string_view text) { + uint64_t revision = 0; + uint32_t cursor = 0; + uint32_t max_length = 0; + bool multiline = false; + TextFilter filter = nullptr; + std::u16string old_text; + std::u16string candidate; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation || text.empty()) { + return false; + } + uint32_t valid_length = 0; + multiline = (g_state.option & OPTION_MULTILINE) != 0; + if (!ValidateText(text.data(), static_cast(text.size()), multiline, + &valid_length) || + valid_length != text.size()) { + return false; + } + std::u16string allowed; + for (const char16_t value: text) { + if (IsAllowedInput(value, g_state.param.type, g_state.option)) { + allowed.push_back(value); + } + } + const size_t available = g_state.max_text_length - g_state.text.size(); + if (allowed.empty() || available == 0) { + return false; + } + allowed.resize(std::min(allowed.size(), available)); + if (!allowed.empty() && allowed.back() >= 0xd800 && allowed.back() <= 0xdbff) { + allowed.pop_back(); + } + if (allowed.empty()) { + return false; + } + revision = g_state.revision; + max_length = g_state.max_text_length; + filter = g_state.param.filter; + old_text = g_state.text; + candidate = old_text; + candidate.insert(g_state.cursor, allowed); + cursor = g_state.cursor + static_cast(allowed.size()); + } + return CommitFilteredText(generation, revision, std::move(old_text), std::move(candidate), + cursor, filter, max_length, multiline); +} + +static bool ApplyBackspace(uint64_t generation) { + uint64_t revision = 0; + uint32_t cursor = 0; + uint32_t max_length = 0; + bool multiline = false; + TextFilter filter = nullptr; + std::u16string old_text; + std::u16string candidate; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation || g_state.cursor == 0) { + return false; + } + uint32_t first = g_state.cursor - 1; + if (first > 0 && g_state.text[first] >= 0xdc00 && g_state.text[first] <= 0xdfff && + g_state.text[first - 1] >= 0xd800 && g_state.text[first - 1] <= 0xdbff) { + first--; + } + revision = g_state.revision; + max_length = g_state.max_text_length; + multiline = (g_state.option & OPTION_MULTILINE) != 0; + filter = g_state.param.filter; + old_text = g_state.text; + candidate = old_text; + candidate.erase(first, g_state.cursor - first); + cursor = first; + } + return CommitFilteredText(generation, revision, std::move(old_text), std::move(candidate), + cursor, filter, max_length, multiline); +} + +static bool ApplyMoveCursor(uint64_t generation, int delta) { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation || delta == 0) { + return false; + } + const uint32_t old_cursor = g_state.cursor; + int next = + std::clamp(static_cast(old_cursor) + delta, 0, static_cast(g_state.text.size())); + if (delta < 0 && next > 0 && next < static_cast(g_state.text.size()) && + g_state.text[next] >= 0xdc00 && g_state.text[next] <= 0xdfff && + g_state.text[next - 1] >= 0xd800 && g_state.text[next - 1] <= 0xdbff) { + next--; + } else if (delta > 0 && next > 0 && next < static_cast(g_state.text.size()) && + g_state.text[next - 1] >= 0xd800 && g_state.text[next - 1] <= 0xdbff && + g_state.text[next] >= 0xdc00 && g_state.text[next] <= 0xdfff) { + next++; + } + g_state.cursor = static_cast(next); + if (g_state.cursor == old_cursor) { + return false; + } + const uint32_t direction = g_state.cursor > old_cursor ? 2 : 1; + const uint32_t steps = static_cast( + std::abs(static_cast(g_state.cursor) - static_cast(old_cursor))); + for (uint32_t i = 0; i < steps; i++) { + QueuedEvent event; + event.event.id = 2; + event.event.param.caret_move = direction; + QueueEventLocked(std::move(event)); + } + UpdateRevisionLocked(); + return true; +} + +static bool ApplyAccept(uint64_t generation) { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation) { + return false; + } + QueuedEvent event; + event.event.id = 5; + return QueueEventLocked(std::move(event)); +} + +static bool ApplyCancel(uint64_t generation) { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation) { + return false; + } + QueuedEvent event; + event.event.id = 4; + return QueueEventLocked(std::move(event)); +} + +bool HostInsertText(uint64_t generation, std::u16string_view text) { + ExternalInput input; + input.action = ExternalAction::Text; + input.text = text; + return HostQueueExternalInput(generation, std::move(input)); +} + +bool HostBackspace(uint64_t generation) { + ExternalInput input; + input.action = ExternalAction::Backspace; + return HostQueueExternalInput(generation, std::move(input)); +} + +bool HostAccept(uint64_t generation) { + ExternalInput input; + input.action = ExternalAction::Accept; + return HostQueueExternalInput(generation, std::move(input)); +} + +bool HostCancel(uint64_t generation) { + ExternalInput input; + input.action = ExternalAction::Cancel; + return HostQueueExternalInput(generation, std::move(input)); +} + +bool HostQueueExternalInput(uint64_t generation, ExternalInput input) { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation) { + return false; + } + if (g_state.external_inputs.size() == EVENT_QUEUE_CAPACITY) { + g_state.event_overflow = true; + return false; + } + g_state.external_inputs.push_back(std::move(input)); + return true; +} + +namespace { + +void ApplyExternalInputs() { + uint64_t generation = 0; + ExtKeyboardFilter filter = nullptr; + int32_t user_id = -1; + bool multiline = false; + std::vector inputs; + { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.external_inputs.empty()) { + return; + } + generation = g_state.generation; + filter = g_state.extended.ext_keyboard_filter; + user_id = g_state.param.user_id; + multiline = (g_state.option & OPTION_MULTILINE) != 0; + inputs.swap(g_state.external_inputs); + } + for (auto& input: inputs) { + { + std::scoped_lock lock(g_mutex); + if (!g_state.open || g_state.generation != generation) { + break; + } + } + input.key.user_id = user_id; + bool accepted = true; + if (filter != nullptr && input.key.status != 0) { + uint16_t keycode = input.key.keycode; + uint32_t status = input.key.status; + if (filter(&input.key, &keycode, &status, nullptr) == 0) { + accepted = ApplyKeyboardFilterOutput(&input, keycode, status, multiline); + } + } + if (!accepted) { + continue; + } + switch (input.action) { + case ExternalAction::None: break; + case ExternalAction::Text: ApplyInsertText(generation, input.text); break; + case ExternalAction::Backspace: ApplyBackspace(generation); break; + case ExternalAction::MoveLeft: ApplyMoveCursor(generation, -1); break; + case ExternalAction::MoveRight: ApplyMoveCursor(generation, 1); break; + case ExternalAction::Cancel: ApplyCancel(generation); break; + case ExternalAction::Accept: ApplyAccept(generation); break; + case ExternalAction::Newline: ApplyInsertText(generation, u"\n"); break; + } + } +} + +} // namespace + +LIB_VERSION("Ime", 1, "Ime", 1, 1); + +LIB_DEFINE(InitPlatform_1_Ime) { + LIB_FUNC("ieCNrVrzKd4", ImeSetText); + LIB_FUNC("WLxUN2WMim8", ImeSetCaret); + LIB_FUNC("WmYDzdC4EHI", ImeParamInit); + LIB_FUNC("ziPDcIjO0Vk", ImeGetPanelSize); + LIB_FUNC("RPydv-Jr1bc", ImeOpen); + LIB_FUNC("-4GCfYdNF1s", ImeUpdate); + LIB_FUNC("TmVP8LzcFcY", ImeClose); + LIB_FUNC("TXYHFRuL8UY", ImeSetTextGeometry); + LIB_FUNC("eaFXjfJv3xs", ImeKeyboardOpen); + LIB_FUNC("PMVehSlfZ94", ImeKeyboardClose); + LIB_FUNC("dKadqZFgKKQ", ImeKeyboardGetResourceId); + LIB_FUNC("VkqLPArfFdc", ImeKeyboardGetInfo); + LIB_FUNC("ua+13Hk9kKs", ImeKeyboardSetMode); +} + +} // namespace Libs::Ime diff --git a/src/libs/ime.h b/src/libs/ime.h new file mode 100644 index 0000000..195bafe --- /dev/null +++ b/src/libs/ime.h @@ -0,0 +1,245 @@ +#ifndef EMULATOR_INCLUDE_EMULATOR_LIBS_IME_H_ +#define EMULATOR_INCLUDE_EMULATOR_LIBS_IME_H_ + +#include "common/abi.h" + +#include +#include +#include +#include + +namespace Libs::Ime { + +constexpr uint32_t WORK_BUFFER_SIZE = 20 * 1024; +constexpr uint32_t MAX_TEXT_LENGTH = 2048; + +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 }; + +struct Event; +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 EventHandler = void(KYTY_SYSV_ABI*)(void* arg, const Event* event); +using ExtKeyboardFilter = int(KYTY_SYSV_ABI*)(const Keycode* source_keycode, uint16_t* out_keycode, + uint32_t* out_status, void* reserved); + +struct Color { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +struct Caret { + float x; + float y; + uint32_t height; + uint32_t index; +}; + +struct TextGeometry { + float x; + float y; + uint32_t width; + uint32_t height; +}; + +struct Rect { + float x; + float y; + uint32_t width; + uint32_t height; +}; + +enum class TextAreaMode : uint32_t { Disable = 0, Edit = 1, Preedit = 2, Select = 3 }; + +struct TextAreaProperty { + TextAreaMode mode; + uint32_t index; + int32_t length; +}; + +struct EditText { + char16_t* str; + uint32_t caret_index; + uint32_t area_num; + TextAreaProperty text_area[4]; +}; + +struct KeyboardResourceIdArray { + int32_t user_id; + uint32_t resource_id[5]; +}; + +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; +}; + +union EventParam { + Rect rect; + EditText text; + uint32_t caret_move; + Keycode keycode; + KeyboardResourceIdArray resource_id_array; + uint64_t reserved[8]; +}; + +struct Event { + uint32_t id; + EventParam param; +}; + +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; + void* work; + void* arg; + EventHandler handler; + int8_t reserved[8]; +}; + +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 KeyboardParam { + uint32_t option; + int8_t reserved1[4]; + void* arg; + EventHandler handler; + int8_t reserved2[8]; +}; + +struct KeyboardInfo { + int32_t user_id; + uint32_t device; + uint32_t type; + uint32_t repeat_delay; + uint32_t repeat_rate; + uint32_t status; + int8_t reserved[12]; +}; + +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(Caret) == 0x10); +static_assert(sizeof(TextGeometry) == 0x10); +static_assert(sizeof(TextAreaProperty) == 0x0c); +static_assert(sizeof(EditText) == 0x40); +static_assert(sizeof(EventParam) == 0x40); +static_assert(alignof(EventParam) == 0x08); +static_assert(sizeof(Event) == 0x48); +static_assert(offsetof(Event, param) == 0x08); +static_assert(sizeof(Param) == 0x60); +static_assert(offsetof(Param, input_text_buffer) == 0x28); +static_assert(offsetof(Param, work) == 0x40); +static_assert(offsetof(Param, handler) == 0x50); +static_assert(sizeof(ExtendedParam) == 0x88); +static_assert(offsetof(ExtendedParam, additional_dictionary_path) == 0x30); +static_assert(sizeof(KeyboardParam) == 0x20); +static_assert(sizeof(KeyboardResourceIdArray) == 0x18); +static_assert(sizeof(KeyboardInfo) == 0x24); +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; +}; + +using VisibilityCallback = void (*)(bool visible, uint64_t generation); + +void KYTY_SYSV_ABI ImeParamInit(Param* param); +int KYTY_SYSV_ABI ImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* height); +int KYTY_SYSV_ABI ImeOpen(const Param* param, const ExtendedParam* extended); +int KYTY_SYSV_ABI ImeUpdate(EventHandler handler); +int KYTY_SYSV_ABI ImeClose(); +int KYTY_SYSV_ABI ImeSetText(const char16_t* text, uint32_t length); +int KYTY_SYSV_ABI ImeSetCaret(const Caret* caret); +int KYTY_SYSV_ABI ImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry); +int KYTY_SYSV_ABI ImeKeyboardOpen(int32_t user_id, const KeyboardParam* param); +int KYTY_SYSV_ABI ImeKeyboardClose(int32_t user_id); +int KYTY_SYSV_ABI ImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids); +int KYTY_SYSV_ABI ImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info); +int KYTY_SYSV_ABI ImeKeyboardSetMode(int32_t user_id, uint32_t mode); + +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 HostAccept(uint64_t generation); +bool HostCancel(uint64_t generation); +bool HostQueueExternalInput(uint64_t generation, ExternalInput input); + +} // namespace Libs::Ime + +#endif // EMULATOR_INCLUDE_EMULATOR_LIBS_IME_H_ diff --git a/src/libs/libNet.cpp b/src/libs/libNet.cpp index 796e51f..1b9db8a 100644 --- a/src/libs/libNet.cpp +++ b/src/libs/libNet.cpp @@ -3804,77 +3804,6 @@ LIB_DEFINE(InitPlatform_1_Random) { } // namespace LibRandom -namespace LibIme { - -LIB_VERSION("Ime", 1, "Ime", 1, 1); - -static int KYTY_SYSV_ABI ImeKeyboardOpen(int user_id, const void* param) { - PRINT_NAME(); - - LOGF("\t user_id = %d\n" - "\t param = 0x%016" PRIx64 "\n", - user_id, reinterpret_cast(param)); - - return 0; -} - -static int KYTY_SYSV_ABI ImeKeyboardClose(int user_id) { - PRINT_NAME(); - - LOGF("\t user_id = %d\n", user_id); - - return 0; -} - -static int KYTY_SYSV_ABI ImeKeyboardGetResourceId(int user_id, void* resource_id_array) { - PRINT_NAME(); - - LOGF("\t user_id = %d\n" - "\t resource_id_array = 0x%016" PRIx64 "\n", - user_id, reinterpret_cast(resource_id_array)); - - return 0; -} - -static int KYTY_SYSV_ABI ImeKeyboardGetInfo(uint32_t resource_id, void* info) { - PRINT_NAME(); - - LOGF("\t resource_id = 0x%08" PRIx32 "\n" - "\t info = 0x%016" PRIx64 "\n", - resource_id, reinterpret_cast(info)); - - return 0; -} - -static int KYTY_SYSV_ABI ImeKeyboardSetMode(int user_id, uint32_t mode) { - PRINT_NAME(); - - LOGF("\t user_id = %d\n" - "\t mode = 0x%08" PRIx32 "\n", - user_id, mode); - - return 0; -} - -static int KYTY_SYSV_ABI ImeUpdate(void* handler) { - PRINT_NAME(); - - // LOGF("\t handler = 0x%016" PRIx64 "\n", reinterpret_cast(handler)); - - return 0; -} - -LIB_DEFINE(InitPlatform_1_Ime) { - LIB_FUNC("eaFXjfJv3xs", LibIme::ImeKeyboardOpen); - LIB_FUNC("PMVehSlfZ94", LibIme::ImeKeyboardClose); - LIB_FUNC("dKadqZFgKKQ", LibIme::ImeKeyboardGetResourceId); - LIB_FUNC("VkqLPArfFdc", LibIme::ImeKeyboardGetInfo); - LIB_FUNC("ua+13Hk9kKs", LibIme::ImeKeyboardSetMode); - LIB_FUNC("-4GCfYdNF1s", LibIme::ImeUpdate); -} - -} // namespace LibIme - namespace LibRemoteplay { LIB_VERSION("Remoteplay", 1, "Remoteplay", 1, 1); @@ -3994,7 +3923,6 @@ LIB_DEFINE(InitPlatform_1_SharePlay) { LIB_DEFINE(InitPlatform_1) { LibRandom::InitPlatform_1_Random(s); - LibIme::InitPlatform_1_Ime(s); LibRemoteplay::InitPlatform_1_Remoteplay(s); LibWebBrowserDialog::InitPlatform_1_WebBrowserDialog(s); LibGameLiveStreaming::InitPlatform_1_GameLiveStreaming(s); diff --git a/src/libs/libs.cpp b/src/libs/libs.cpp index ea81eeb..36e4dc8 100644 --- a/src/libs/libs.cpp +++ b/src/libs/libs.cpp @@ -34,6 +34,10 @@ namespace LibKeyboard { LIB_DEFINE(InitKeyboard_1); } // namespace LibKeyboard +namespace Ime { +LIB_DEFINE(InitPlatform_1_Ime); +} // namespace Ime + namespace LibUlt { LIB_DEFINE(InitUlt_1); } // namespace LibUlt @@ -89,6 +93,7 @@ void InitAll(Loader::SymbolDatabase* s) { LIB_LOAD(InitLibKernel_1); LIB_LOAD(LibMouse::InitMouse_1); LIB_LOAD(LibKeyboard::InitKeyboard_1); + LIB_LOAD(Ime::InitPlatform_1_Ime); LIB_LOAD(InitNet_1); LIB_LOAD(InitPad_1); LIB_LOAD(InitPlayGo_1);