diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 99f769f79..0e778ba7e 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -46,24 +46,24 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService .ToDictionary(x => x.Item1, x => x.Name); private static readonly UnicodeRange[] HanRange = - { + [ UnicodeRanges.CjkRadicalsSupplement, UnicodeRanges.CjkSymbolsandPunctuation, UnicodeRanges.CjkUnifiedIdeographsExtensionA, UnicodeRanges.CjkUnifiedIdeographs, UnicodeRanges.CjkCompatibilityIdeographs, - UnicodeRanges.CjkCompatibilityForms, + UnicodeRanges.CjkCompatibilityForms // No more; Extension B~ are outside BMP range - }; + ]; private static readonly UnicodeRange[] HangulRange = - { + [ UnicodeRanges.HangulJamo, UnicodeRanges.HangulSyllables, UnicodeRanges.HangulCompatibilityJamo, UnicodeRanges.HangulJamoExtendedA, - UnicodeRanges.HangulJamoExtendedB, - }; + UnicodeRanges.HangulJamoExtendedB + ]; [ServiceManager.ServiceDependency] private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); @@ -109,24 +109,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService private bool updateInputLanguage = true; private bool updateImeStatusAgain; - [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] - static DalamudIme() - { - nint cimgui; - try - { - _ = ImGui.GetCurrentContext(); - - cimgui = Process.GetCurrentProcess().Modules.Cast() - .First(x => x.ModuleName == "cimgui.dll") - .BaseAddress; - } - catch - { - return; - } - } - [ServiceManager.ServiceConstructor] private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws) { @@ -170,11 +152,11 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; var textState = GetInputTextState(); - if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0) + if (textState.ID == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) return true; - if (textState->CursorAnim <= 0) + if (textState.CursorAnim <= 0) return true; - return textState->CursorAnim % 1.2f <= 0.8f; + return textState.CursorAnim % 1.2f <= 0.8f; } } @@ -227,11 +209,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService } } - private static ImGuiInputTextStateWrapper* GetInputTextState() - { - var ctx = ImGui.GetCurrentContext(); - return (ImGuiInputTextStateWrapper*)&ctx.Handle->InputTextState; - } + private static ImGuiInputTextStatePtr GetInputTextState() => + (ImGuiInputTextState*)((nint)ImGui.GetCurrentContext().Handle + 0x4588); private static (string String, bool Supported) ToUcs2(char* data, int nc = -1) { @@ -332,7 +311,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService try { var textState = GetInputTextState(); - var invalidTarget = textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0; + var invalidTarget = textState.ID == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; #if IMEDEBUG switch (args.Message) @@ -564,17 +543,17 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService var textState = GetInputTextState(); if (this.temporaryUndoSelection is not null) { - textState->Undo(); - textState->SelectionTuple = this.temporaryUndoSelection.Value; + textState.Undo(); + textState.SetSelectionTuple(this.temporaryUndoSelection.Value); this.temporaryUndoSelection = null; } - textState->SanitizeSelectionRange(); - if (textState->ReplaceSelectionAndPushUndo(newString)) - this.temporaryUndoSelection = textState->SelectionTuple; + textState.SanitizeSelectionRange(); + if (textState.ReplaceSelectionAndPushUndo(newString)) + this.temporaryUndoSelection = textState.GetSelectionTuple(); // Put the cursor at the beginning, so that the candidate window appears aligned with the text. - textState->SetSelectionRange(textState->SelectionTuple.Start, newString.Length, 0); + textState.SetSelectionRange(textState.GetSelectionTuple().Start, newString.Length, 0); if (finalCommit) { @@ -621,7 +600,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService this.temporaryUndoSelection = null; var textState = GetInputTextState(); - textState->Stb.SelectStart = textState->Stb.Cursor = textState->Stb.SelectEnd; + textState.Stb.SelectStart = textState.Stb.Cursor = textState.Stb.SelectEnd; this.candidateStrings.Clear(); this.immCandNative = default; @@ -931,185 +910,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService } } - /// - /// Ported from imstb_textedit.h. - /// - [StructLayout(LayoutKind.Sequential, Size = 0xE2C)] - private struct StbTextEditState - { - /// - /// Position of the text cursor within the string. - /// - public int Cursor; - - /// - /// Selection start point. - /// - public int SelectStart; - - /// - /// selection start and end point in characters; if equal, no selection. - /// - /// - /// Note that start may be less than or greater than end (e.g. when dragging the mouse, - /// start is where the initial click was, and you can drag in either direction.) - /// - public int SelectEnd; - - /// - /// Each text field keeps its own insert mode state. - /// To keep an app-wide insert mode, copy this value in/out of the app state. - /// - public byte InsertMode; - - /// - /// Page size in number of row. - /// This value MUST be set to >0 for pageup or pagedown in multilines documents. - /// - public int RowCountPerPage; - - // Remainder is stb-private data. - } - - [StructLayout(LayoutKind.Sequential)] - private struct ImGuiInputTextStateWrapper - { - public uint Id; - public int CurLenW; - public int CurLenA; - public ImVector TextWRaw; - public ImVector TextARaw; - public ImVector InitialTextARaw; - public bool TextAIsValid; - public int BufCapacityA; - public float ScrollX; - public StbTextEditState Stb; - public float CursorAnim; - public bool CursorFollow; - public bool SelectedAllMouseLock; - public bool Edited; - public ImGuiInputTextFlags Flags; - - public ImVectorWrapper TextW => new((ImVector*)&this.ThisWrapperPtr->TextWRaw); - - public (int Start, int End, int Cursor) SelectionTuple - { - get => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor); - set => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor) = value; - } - - private ImGuiInputTextStateWrapper* ThisWrapperPtr => (ImGuiInputTextStateWrapper*)Unsafe.AsPointer(ref this); - - private ImGuiInputTextState* ThisPtr => (ImGuiInputTextState*)Unsafe.AsPointer(ref this); - - public void SetSelectionRange(int offset, int length, int relativeCursorOffset) - { - this.Stb.SelectStart = offset; - this.Stb.SelectEnd = offset + length; - if (relativeCursorOffset >= 0) - this.Stb.Cursor = this.Stb.SelectStart + relativeCursorOffset; - else - this.Stb.Cursor = this.Stb.SelectEnd + 1 + relativeCursorOffset; - this.SanitizeSelectionRange(); - } - - public void SanitizeSelectionRange() - { - ref var s = ref this.Stb.SelectStart; - ref var e = ref this.Stb.SelectEnd; - ref var c = ref this.Stb.Cursor; - s = Math.Clamp(s, 0, this.CurLenW); - e = Math.Clamp(e, 0, this.CurLenW); - c = Math.Clamp(c, 0, this.CurLenW); - if (s == e) - s = e = c; - if (s > e) - (s, e) = (e, s); - } - - public void Undo() => ImGuiP.Custom_StbTextUndo(this.ThisPtr); - - public bool MakeUndoReplace(int offset, int oldLength, int newLength) - { - if (oldLength == 0 && newLength == 0) - return false; - - ImGuiP.Custom_StbTextMakeUndoReplace(this.ThisPtr, offset, oldLength, newLength); - return true; - } - - public bool ReplaceSelectionAndPushUndo(ReadOnlySpan newText) - { - var off = this.Stb.SelectStart; - var len = this.Stb.SelectEnd - this.Stb.SelectStart; - return this.MakeUndoReplace(off, len, newText.Length) && this.ReplaceChars(off, len, newText); - } - - public bool ReplaceChars(int pos, int len, ReadOnlySpan newText) - { - this.DeleteChars(pos, len); - return this.InsertChars(pos, newText); - } - - // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS - public void DeleteChars(int pos, int n) - { - if (n == 0) - return; - - var dst = this.TextW.Data + pos; - - // We maintain our buffer length in both UTF-8 and wchar formats - this.Edited = true; - this.CurLenA -= Encoding.UTF8.GetByteCount(dst, n); - this.CurLenW -= n; - - // Offset remaining text (FIXME-OPT: Use memmove) - var src = this.TextW.Data + pos + n; - int i; - for (i = 0; src[i] != 0; i++) - dst[i] = src[i]; - dst[i] = '\0'; - } - - // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS - public bool InsertChars(int pos, ReadOnlySpan newText) - { - if (newText.Length == 0) - return true; - - var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; - var textLen = this.CurLenW; - Debug.Assert(pos <= textLen, "pos <= text_len"); - - var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText); - if (!isResizable && newTextLenUtf8 + this.CurLenA + 1 > this.BufCapacityA) - return false; - - // Grow internal buffer if needed - if (newText.Length + textLen + 1 > this.TextW.Length) - { - if (!isResizable) - return false; - - Debug.Assert(textLen < this.TextW.Length, "text_len < this.TextW.Length"); - this.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1); - } - - var text = this.TextW.DataSpan; - if (pos != textLen) - text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); - newText.CopyTo(text[pos..]); - - this.Edited = true; - this.CurLenW += newText.Length; - this.CurLenA += newTextLenUtf8; - this.TextW[this.CurLenW] = '\0'; - - return true; - } - } - #if IMEDEBUG private static class ImeDebug { diff --git a/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs b/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs new file mode 100644 index 000000000..ab5fdaac8 --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs @@ -0,0 +1,123 @@ +using System.Diagnostics; +using System.Text; + +using Dalamud.Bindings.ImGui; + +namespace Dalamud.Interface.Internal; + +#pragma warning disable SA1600 +internal static unsafe class ImGuiInputTextStatePtrExtensions +{ + public static (int Start, int End, int Cursor) GetSelectionTuple(this ImGuiInputTextStatePtr self) => + (self.Stb.SelectStart, self.Stb.SelectEnd, self.Stb.Cursor); + + public static void SetSelectionTuple(this ImGuiInputTextStatePtr self, (int Start, int End, int Cursor) value) => + (self.Stb.SelectStart, self.Stb.SelectEnd, self.Stb.Cursor) = value; + + public static void SetSelectionRange(this ImGuiInputTextStatePtr self, int offset, int length, int relativeCursorOffset) + { + self.Stb.SelectStart = offset; + self.Stb.SelectEnd = offset + length; + if (relativeCursorOffset >= 0) + self.Stb.Cursor = self.Stb.SelectStart + relativeCursorOffset; + else + self.Stb.Cursor = self.Stb.SelectEnd + 1 + relativeCursorOffset; + self.SanitizeSelectionRange(); + } + + public static void SanitizeSelectionRange(this ImGuiInputTextStatePtr self) + { + ref var s = ref self.Stb.SelectStart; + ref var e = ref self.Stb.SelectEnd; + ref var c = ref self.Stb.Cursor; + s = Math.Clamp(s, 0, self.CurLenW); + e = Math.Clamp(e, 0, self.CurLenW); + c = Math.Clamp(c, 0, self.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + } + + public static void Undo(this ImGuiInputTextStatePtr self) => ImGuiP.Custom_StbTextUndo(self); + + public static bool MakeUndoReplace(this ImGuiInputTextStatePtr self, int offset, int oldLength, int newLength) + { + if (oldLength == 0 && newLength == 0) + return false; + + ImGuiP.Custom_StbTextMakeUndoReplace(self, offset, oldLength, newLength); + return true; + } + + public static bool ReplaceSelectionAndPushUndo(this ImGuiInputTextStatePtr self, ReadOnlySpan newText) + { + var off = self.Stb.SelectStart; + var len = self.Stb.SelectEnd - self.Stb.SelectStart; + return self.MakeUndoReplace(off, len, newText.Length) && self.ReplaceChars(off, len, newText); + } + + public static bool ReplaceChars(this ImGuiInputTextStatePtr self, int pos, int len, ReadOnlySpan newText) + { + self.DeleteChars(pos, len); + return self.InsertChars(pos, newText); + } + + // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS + public static void DeleteChars(this ImGuiInputTextStatePtr self, int pos, int n) + { + if (n == 0) + return; + + var dst = (char*)self.TextW.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + self.Edited = true; + self.CurLenA -= Encoding.UTF8.GetByteCount(dst, n); + self.CurLenW -= n; + + // Offset remaining text (FIXME-OPT: Use memmove) + var src = (char*)self.TextW.Data + pos + n; + int i; + for (i = 0; src[i] != 0; i++) + dst[i] = src[i]; + dst[i] = '\0'; + } + + // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS + public static bool InsertChars(this ImGuiInputTextStatePtr self, int pos, ReadOnlySpan newText) + { + if (newText.Length == 0) + return true; + + var isResizable = (self.Flags & ImGuiInputTextFlags.CallbackResize) != 0; + var textLen = self.CurLenW; + Debug.Assert(pos <= textLen, "pos <= text_len"); + + var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText); + if (!isResizable && newTextLenUtf8 + self.CurLenA + 1 > self.BufCapacityA) + return false; + + // Grow internal buffer if needed + if (newText.Length + textLen + 1 > self.TextW.Size) + { + if (!isResizable) + return false; + + Debug.Assert(textLen < self.TextW.Size, "text_len < self.TextW.Length"); + self.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1); + } + + var text = new Span(self.TextW.Data, self.TextW.Size); + if (pos != textLen) + text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); + newText.CopyTo(text[pos..]); + + self.Edited = true; + self.CurLenW += newText.Length; + self.CurLenA += newTextLenUtf8; + self.TextW[self.CurLenW] = '\0'; + + return true; + } +}