#include "graphics/presentation/imeOverlay.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/ime.h" #include "libs/imeDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { namespace CoreIme = Libs::Ime; namespace DialogIme = Libs::Dialog::ImeDialog; namespace Ime { using Type = ImeCommon::Type; using EnterLabel = ImeCommon::EnterLabel; using Alignment = ImeCommon::Alignment; using ExternalAction = ImeCommon::ExternalAction; using ExternalInput = ImeCommon::ExternalInput; using HostSnapshot = ImeCommon::HostSnapshot; constexpr uint32_t OPTION_MULTILINE = ImeCommon::OPTION_MULTILINE; constexpr uint32_t OPTION_NO_AUTO_CAPITALIZE = ImeCommon::OPTION_NO_AUTO_CAPITALIZE; constexpr uint32_t OPTION_PASSWORD = ImeCommon::OPTION_PASSWORD; constexpr uint32_t OPTION_FIXED_POSITION = ImeCommon::OPTION_FIXED_POSITION; constexpr uint32_t OPTION_DISABLE_POSITION_ADJ = ImeCommon::OPTION_DISABLE_POSITION_ADJUST; constexpr uint32_t OPTION_USE_OVER_2K = ImeCommon::OPTION_USE_OVER_2K; constexpr uint32_t DISABLE_DEVICE_CONTROLLER = ImeCommon::DISABLE_DEVICE_CONTROLLER; constexpr uint32_t DISABLE_DEVICE_EXT_KEYBOARD = ImeCommon::DISABLE_DEVICE_EXT_KEYBOARD; 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; } if (CoreIme::GetHostSnapshot(snapshot)) { snapshot->generation = PackCoreGeneration(snapshot->generation); return true; } return DialogIme::GetHostSnapshot(snapshot); } 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) { return IsCoreGeneration(generation) ? CoreIme::HostQueueExternalInput(UnpackGeneration(generation), std::move(input)) : DialogIme::HostQueueExternalInput(generation, std::move(input)); } } // namespace Ime 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 RefreshVisibility() { std::scoped_lock visibility_lock(g_visibility_mutex); if (!g_input_lifecycle_active) { return; } 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) { 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) { 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++; } } } 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; 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 InitializeImeInput() { 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); } CoreIme::SetVisibilityCallback(OnCoreVisibilityChanged); DialogIme::SetVisibilityCallback(OnDialogVisibilityChanged); RefreshVisibility(); } void ShutdownImeInput() { CoreIme::SetVisibilityCallback(nullptr); DialogIme::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(); } } 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 {}; { 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 ImeOverlay::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 IME 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("##Ime", 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; }; ImeOverlay::ImeOverlay(GraphicContext& graphics): m_impl(std::make_unique(graphics)) {} ImeOverlay::~ImeOverlay() = default; bool ImeOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) { return m_impl->PrepareFrame(extent, format, image_count); } void ImeOverlay::Record(vk::CommandBuffer command, vk::ImageView target) { m_impl->Record(command, target); } void ImeOverlay::ReleaseVulkan() { m_impl->ReleaseVulkan(); } } // namespace Libs::Graphics