From d0da181032dd6ebc0ee36d6cfe29c8258eb09456 Mon Sep 17 00:00:00 2001 From: Patrick Dawson Date: Sun, 1 Sep 2024 04:20:04 +0200 Subject: [PATCH] Add IME support --- .../imgui-godot/ImGuiGodot/ImGuiController.cs | 3 +- addons/imgui-godot/ImGuiGodot/ImGuiGD.cs | 9 +- .../imgui-godot/ImGuiGodot/Internal/Input.cs | 28 ++++- .../imgui-godot/ImGuiGodot/Internal/State.cs | 23 ++++ doc/examples/GdsDemo/demo.gd | 116 +++++++++--------- doc/examples/GdsDemo/project.godot | 3 +- gdext/godot-cpp | 2 +- gdext/src/Context.cpp | 16 +++ gdext/src/ImGuiAPI.h | 2 +- gdext/src/ImGuiController.cpp | 4 +- gdext/src/Input.cpp | 30 +++-- 11 files changed, 158 insertions(+), 78 deletions(-) diff --git a/addons/imgui-godot/ImGuiGodot/ImGuiController.cs b/addons/imgui-godot/ImGuiGodot/ImGuiController.cs index 69f2f801..0c03fbd0 100644 --- a/addons/imgui-godot/ImGuiGodot/ImGuiController.cs +++ b/addons/imgui-godot/ImGuiGodot/ImGuiController.cs @@ -11,6 +11,7 @@ public partial class ImGuiController : Node public static ImGuiController Instance { get; private set; } = null!; private ImGuiControllerHelper _helper = null!; public Node Signaler { get; private set; } = null!; + private readonly StringName _signalName = "imgui_layout"; private sealed partial class ImGuiControllerHelper : Node { @@ -78,7 +79,7 @@ public override void _ExitTree() public override void _Process(double delta) { - Signaler.EmitSignal("imgui_layout"); + Signaler.EmitSignal(_signalName); Internal.State.Instance.Render(); Internal.State.Instance.InProcessFrame = false; } diff --git a/addons/imgui-godot/ImGuiGodot/ImGuiGD.cs b/addons/imgui-godot/ImGuiGodot/ImGuiGD.cs index 3a2c6b2a..dc4727fa 100644 --- a/addons/imgui-godot/ImGuiGodot/ImGuiGD.cs +++ b/addons/imgui-godot/ImGuiGodot/ImGuiGD.cs @@ -1,14 +1,13 @@ #if GODOT_PC #nullable enable using Godot; -using ImGuiGodot.Internal; using System; namespace ImGuiGodot; public static class ImGuiGD { - private static readonly IBackend _backend; + private static readonly Internal.IBackend _backend; /// /// Deadzone for all axes @@ -44,7 +43,9 @@ public static bool Visible static ImGuiGD() { - _backend = ClassDB.ClassExists("ImGuiGD") ? new BackendNative() : new BackendNet(); + _backend = ClassDB.ClassExists("ImGuiGD") + ? new Internal.BackendNative() + : new Internal.BackendNet(); } public static IntPtr BindTexture(Texture2D tex) @@ -115,7 +116,7 @@ public static void SetMainViewport(Viewport vp) /// public static bool ToolInit() { - if (_backend is BackendNative nbe) + if (_backend is Internal.BackendNative nbe) { nbe.ToolInit(); return true; diff --git a/addons/imgui-godot/ImGuiGodot/Internal/Input.cs b/addons/imgui-godot/ImGuiGodot/Internal/Input.cs index 3ad67c72..668081a4 100644 --- a/addons/imgui-godot/ImGuiGodot/Internal/Input.cs +++ b/addons/imgui-godot/ImGuiGodot/Internal/Input.cs @@ -15,6 +15,7 @@ internal class Input private Vector2 _mouseWheel = Vector2.Zero; private ImGuiMouseCursor _currentCursor = ImGuiMouseCursor.None; private readonly bool _hasMouse = DisplayServer.HasFeature(DisplayServer.Feature.Mouse); + private bool _takingTextInput = false; protected virtual void UpdateMousePos(ImGuiIOPtr io) { @@ -140,6 +141,15 @@ protected bool HandleEvent(InputEvent evt) var io = ImGui.GetIO(); bool consumed = false; + if (io.WantTextInput && !_takingTextInput) + { + // avoid IME issues if a text input control was focused + State.Instance.Layer.GetViewport().GuiReleaseFocus(); + + // TODO: show virtual keyboard? + } + _takingTextInput = io.WantTextInput; + if (evt is InputEventMouseMotion mm) { consumed = io.WantCaptureMouse; @@ -184,15 +194,19 @@ protected bool HandleEvent(InputEvent evt) { UpdateKeyMods(io); ImGuiKey igk = ConvertKey(k.Keycode); + bool pressed = k.Pressed; + long unicode = k.Unicode; + if (igk != ImGuiKey.None) { - io.AddKeyEvent(igk, k.Pressed); + io.AddKeyEvent(igk, pressed); + } - if (k.Pressed && k.Unicode != 0 && io.WantTextInput) - { - io.AddInputCharacter((uint)k.Unicode); - } + if (pressed && unicode != 0 && io.WantTextInput) + { + io.AddInputCharacterUTF16((ushort)unicode); } + consumed = io.WantCaptureKeyboard || io.WantTextInput; k.Dispose(); } @@ -268,6 +282,10 @@ public static void ProcessNotification(long what) case MainLoop.NotificationApplicationFocusOut: ImGui.GetIO().AddFocusEvent(false); break; + case MainLoop.NotificationOsImeUpdate: + // workaround for Godot suppressing key up events during IME + ImGui.GetIO().ClearInputKeys(); + break; } } diff --git a/addons/imgui-godot/ImGuiGodot/Internal/State.cs b/addons/imgui-godot/ImGuiGodot/Internal/State.cs index fa47a075..af39d2a6 100644 --- a/addons/imgui-godot/ImGuiGodot/Internal/State.cs +++ b/addons/imgui-godot/ImGuiGodot/Internal/State.cs @@ -32,6 +32,13 @@ private enum RendererType internal static State Instance { get; set; } = null!; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void PlatformSetImeDataFn( + nint ctx, + ImGuiViewportPtr vp, + ImGuiPlatformImeDataPtr data); + private static readonly PlatformSetImeDataFn _setImeData = SetImeData; + public State(IRenderer renderer) { Renderer = renderer; @@ -63,6 +70,7 @@ public State(IRenderer renderer) { io.NativePtr->BackendPlatformName = (byte*)_backendName; io.NativePtr->BackendRendererName = (byte*)_rendererName; + io.NativePtr->PlatformSetImeDataFn = Marshal.GetFunctionPointerForDelegate(_setImeData); } Viewports = new Viewports(); @@ -184,5 +192,20 @@ public void Render() ImGui.UpdatePlatformWindows(); Renderer.Render(); } + + private static void SetImeData(nint ctx, ImGuiViewportPtr vp, ImGuiPlatformImeDataPtr data) + { + int windowID = (int)vp.PlatformHandle; + + DisplayServer.WindowSetImeActive(data.WantVisible, windowID); + if (data.WantVisible) + { + Vector2I pos = new( + (int)(data.InputPos.X - vp.Pos.X), + (int)(data.InputPos.Y - vp.Pos.Y + data.InputLineHeight) + ); + DisplayServer.WindowSetImePosition(pos, windowID); + } + } } #endif diff --git a/doc/examples/GdsDemo/demo.gd b/doc/examples/GdsDemo/demo.gd index 42058a61..8b398847 100644 --- a/doc/examples/GdsDemo/demo.gd +++ b/doc/examples/GdsDemo/demo.gd @@ -12,6 +12,8 @@ var ms_selection := [] var table_items := [] func _ready(): + Engine.max_fps = 120 + var io := ImGui.GetIO() io.ConfigFlags |= ImGui.ConfigFlags_ViewportsEnable @@ -30,70 +32,70 @@ func _process(_delta: float) -> void: var gdver: String = Engine.get_version_info()["string"] - ImGui.Begin("Demo") - ImGui.Text("ImGui in") - ImGui.SameLine() - ImGui.TextLinkOpenURLEx("Godot %s" % gdver, "https://www.godotengine.org") - ImGui.Text("mem %.1f KiB / peak %.1f KiB" % [ - OS.get_static_memory_usage() / 1024.0, - OS.get_static_memory_peak_usage() / 1024.0]) - ImGui.Separator() - - ImGui.DragFloat("myfloat", myfloat) - ImGui.Text(str(myfloat[0])) - ImGui.InputText("mystr", mystr, 32) - ImGui.Text(mystr[0]) - - ImGui.PlotHistogram("histogram", values, values.size()) - ImGui.PlotLines("lines", values, values.size()) - ImGui.ListBox("choices", current_item, items, items.size()) - ImGui.Combo("combo", current_item, items) - ImGui.Text("choice = %s" % items[current_item[0]]) - - ImGui.SeparatorText("Multi-Select") - if ImGui.BeginChild("MSItems", Vector2(0,0), ImGui.ChildFlags_FrameStyle): - var flags := ImGui.MultiSelectFlags_ClearOnEscape | ImGui.MultiSelectFlags_BoxSelect1d - var ms_io := ImGui.BeginMultiSelectEx(flags, ms_selection.size(), ms_items.size()) - apply_selection_requests(ms_io) - for i in range(items.size()): - var is_selected := ms_selection.has(i) - ImGui.SetNextItemSelectionUserData(i) - ImGui.SelectableEx(ms_items[i], is_selected) - ms_io = ImGui.EndMultiSelect() - apply_selection_requests(ms_io) + if ImGui.Begin("Demo"): + ImGui.Text("ImGui in") + ImGui.SameLine() + ImGui.TextLinkOpenURLEx("Godot %s" % gdver, "https://www.godotengine.org") + ImGui.Text("mem %.1f KiB / peak %.1f KiB" % [ + OS.get_static_memory_usage() / 1024.0, + OS.get_static_memory_peak_usage() / 1024.0]) + ImGui.Separator() + + ImGui.DragFloat("myfloat", myfloat) + ImGui.Text(str(myfloat[0])) + ImGui.InputText("mystr", mystr, 32) + ImGui.Text(mystr[0]) + + ImGui.PlotHistogram("histogram", values, values.size()) + ImGui.PlotLines("lines", values, values.size()) + ImGui.ListBox("choices", current_item, items, items.size()) + ImGui.Combo("combo", current_item, items) + ImGui.Text("choice = %s" % items[current_item[0]]) + + ImGui.SeparatorText("Multi-Select") + if ImGui.BeginChild("MSItems", Vector2(0,0), ImGui.ChildFlags_FrameStyle): + var flags := ImGui.MultiSelectFlags_ClearOnEscape | ImGui.MultiSelectFlags_BoxSelect1d + var ms_io := ImGui.BeginMultiSelectEx(flags, ms_selection.size(), ms_items.size()) + apply_selection_requests(ms_io) + for i in range(items.size()): + var is_selected := ms_selection.has(i) + ImGui.SetNextItemSelectionUserData(i) + ImGui.SelectableEx(ms_items[i], is_selected) + ms_io = ImGui.EndMultiSelect() + apply_selection_requests(ms_io) ImGui.EndChild() ImGui.End() - ImGui.Begin("Sortable Table") - if ImGui.BeginTable("sortable_table", 2, ImGui.TableFlags_Sortable): - ImGui.TableSetupColumn("ID", ImGui.TableColumnFlags_DefaultSort) - ImGui.TableSetupColumn("Name") - ImGui.TableSetupScrollFreeze(0, 1) - ImGui.TableHeadersRow() - - var sort_specs := ImGui.TableGetSortSpecs() - if sort_specs.SpecsDirty: - for spec: ImGuiTableColumnSortSpecsPtr in sort_specs.Specs: - var col := spec.ColumnIndex - if spec.SortDirection == ImGui.SortDirection_Ascending: - table_items.sort_custom(func(lhs, rhs): return lhs[col] < rhs[col]) - else: - table_items.sort_custom(func(lhs, rhs): return lhs[col] > rhs[col]) - sort_specs.SpecsDirty = false - - for i in range(table_items.size()): - ImGui.TableNextRow() - ImGui.TableNextColumn() - ImGui.Text("%d" % table_items[i][0]) - ImGui.TableNextColumn() - ImGui.Text(table_items[i][1]) - ImGui.EndTable() + if ImGui.Begin("Sortable Table"): + if ImGui.BeginTable("sortable_table", 2, ImGui.TableFlags_Sortable): + ImGui.TableSetupColumn("ID", ImGui.TableColumnFlags_DefaultSort) + ImGui.TableSetupColumn("Name") + ImGui.TableSetupScrollFreeze(0, 1) + ImGui.TableHeadersRow() + + var sort_specs := ImGui.TableGetSortSpecs() + if sort_specs.SpecsDirty: + for spec: ImGuiTableColumnSortSpecsPtr in sort_specs.Specs: + var col := spec.ColumnIndex + if spec.SortDirection == ImGui.SortDirection_Ascending: + table_items.sort_custom(func(lhs, rhs): return lhs[col] < rhs[col]) + else: + table_items.sort_custom(func(lhs, rhs): return lhs[col] > rhs[col]) + sort_specs.SpecsDirty = false + + for i in range(table_items.size()): + ImGui.TableNextRow() + ImGui.TableNextColumn() + ImGui.Text("%d" % table_items[i][0]) + ImGui.TableNextColumn() + ImGui.Text(table_items[i][1]) + ImGui.EndTable() ImGui.End() ImGui.SetNextWindowClass(wc_topmost) ImGui.SetNextWindowSize(Vector2(200, 200), ImGui.Cond_Once) - ImGui.Begin("topmost viewport window") - ImGui.TextWrapped("when this is a viewport window outside the main window, it will stay on top") + if ImGui.Begin("topmost viewport window"): + ImGui.TextWrapped("when this is a viewport window outside the main window, it will stay on top") ImGui.End() func _physics_process(_delta: float) -> void: diff --git a/doc/examples/GdsDemo/project.godot b/doc/examples/GdsDemo/project.godot index 21fc64b9..6d9fa442 100644 --- a/doc/examples/GdsDemo/project.godot +++ b/doc/examples/GdsDemo/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="GdsDemo" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.2", "Forward Plus") +config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" [autoload] @@ -22,6 +22,7 @@ ImGuiRoot="*res://addons/imgui-godot/data/ImGuiRoot.tscn" [display] window/subwindows/embed_subwindows=false +window/vsync/vsync_mode=0 [editor_plugins] diff --git a/gdext/godot-cpp b/gdext/godot-cpp index d6e5286c..1f9a0b71 160000 --- a/gdext/godot-cpp +++ b/gdext/godot-cpp @@ -1 +1 @@ -Subproject commit d6e5286cc19bbd5b2c626207d3b01a8f145c0f76 +Subproject commit 1f9a0b7171b4a3c89139236653b13318d312ab39 diff --git a/gdext/src/Context.cpp b/gdext/src/Context.cpp index d8032ac5..cff56a16 100644 --- a/gdext/src/Context.cpp +++ b/gdext/src/Context.cpp @@ -11,6 +11,21 @@ namespace { std::unique_ptr ctx; const char* PlatformName = "godot4"; + +void SetImeData(ImGuiContext* ctx, ImGuiViewport* vp, ImGuiPlatformImeData* data) +{ + DisplayServer* DS = DisplayServer::get_singleton(); + const int32_t windowID = (int32_t)(intptr_t)vp->PlatformHandle; + + DS->window_set_ime_active(data->WantVisible, windowID); + if (data->WantVisible) + { + Vector2i pos; + pos.x = data->InputPos.x - vp->Pos.x; + pos.y = data->InputPos.y - vp->Pos.y + data->InputLineHeight; + DS->window_set_ime_position(pos, windowID); + } +} } // namespace Context* GetContext() @@ -31,6 +46,7 @@ Context::Context(std::unique_ptr r) io.BackendPlatformName = PlatformName; io.BackendRendererName = renderer->Name(); + io.PlatformSetImeDataFn = SetImeData; viewports = std::make_unique(); } diff --git a/gdext/src/ImGuiAPI.h b/gdext/src/ImGuiAPI.h index 2a889f77..f9896504 100644 --- a/gdext/src/ImGuiAPI.h +++ b/gdext/src/ImGuiAPI.h @@ -50,7 +50,7 @@ struct GdsPtr { if (bufhash != std::hash{}({buf.begin(), buf.end()})) { - arr[0] = String(buf.data()); + arr[0] = String::utf8(buf.data()); } } diff --git a/gdext/src/ImGuiController.cpp b/gdext/src/ImGuiController.cpp index 886dd4f2..378ad3b1 100644 --- a/gdext/src/ImGuiController.cpp +++ b/gdext/src/ImGuiController.cpp @@ -124,8 +124,10 @@ void ImGuiController::_process(double delta) } #endif + static const StringName signalName("imgui_layout"); + emit_signal(signalName); + Context* ctx = GetContext(); - emit_signal("imgui_layout"); ctx->Render(); ctx->inProcessFrame = false; } diff --git a/gdext/src/Input.cpp b/gdext/src/Input.cpp index 0200a0d7..8cd4c003 100644 --- a/gdext/src/Input.cpp +++ b/gdext/src/Input.cpp @@ -28,6 +28,7 @@ struct Input::Impl Vector2 mouseWheel; ImGuiMouseCursor currentCursor = ImGuiMouseCursor_None; bool hasMouse = false; + bool takingTextInput = false; }; namespace { @@ -199,9 +200,17 @@ void Input::ProcessSubViewportWidget(const Ref& evt) bool Input::HandleEvent(const Ref& evt) { ImGuiIO& io = ImGui::GetIO(); - bool consumed = false; + if (io.WantTextInput && !impl->takingTextInput) + { + // avoid IME issues if a text input control was focused + GetContext()->layer->get_viewport()->gui_release_focus(); + + // TODO: show virtual keyboard? + } + impl->takingTextInput = io.WantTextInput; + if (Ref mm = evt; mm.is_valid()) { consumed = io.WantCaptureMouse; @@ -247,14 +256,17 @@ bool Input::HandleEvent(const Ref& evt) { UpdateKeyMods(io); ImGuiKey igk = ToImGuiKey(k->get_keycode()); + bool pressed = k->is_pressed(); + uint32_t unicode = k->get_unicode(); + if (igk != ImGuiKey_None) { - bool pressed = k->is_pressed(); - io.AddKeyEvent(igk, k->is_pressed()); - if (pressed && k->get_unicode() != 0 && io.WantTextInput) - { - io.AddInputCharacter(k->get_unicode()); - } + io.AddKeyEvent(igk, pressed); + } + + if (pressed && unicode != 0 && io.WantTextInput) + { + io.AddInputCharacterUTF16(unicode); } consumed = io.WantCaptureKeyboard || io.WantTextInput; } @@ -333,6 +345,10 @@ void Input::ProcessNotification(int what) case Node::NOTIFICATION_APPLICATION_FOCUS_OUT: ImGui::GetIO().AddFocusEvent(false); break; + case MainLoop::NOTIFICATION_OS_IME_UPDATE: + // workaround for Godot suppressing key up events during IME + ImGui::GetIO().ClearInputKeys(); + break; }; }