Dalamud/Dalamud/Interface/Internal/DalamudIme.cs
srkizer 87b9edb448
Add IInternal/PublicDisposableService (#1696)
* Add IInternal/PublicDisposableService

Plugins are exposed interfaces that are not inherited from
`IDisposable`, but services implementing plugin interfaces often
implement `IDisposable`. Some plugins may try to call
`IDisposable.Dispose` on everything provided, and it also is possible to
use `using` clause too eagerly while working on Dalamud itself, such as
writing `using var smth = await Service<SomeService>.GetAsync();`. Such
behaviors often lead to a difficult-to-debug errors, and making those
services either not an `IDisposable` or making `IDisposable.Dispose` do
nothing if the object has been loaded would prevent such errors. As
`ServiceManager` must be the only class dealing with construction and
disposal of services, `IInternalDisposableService` has been added to
limit who can dispose the object. `IPublicDisposableService` also has
been added to classes that can be constructed and accessed directly by
plugins; for those, `Dispose` will be ignored if the instance is a
service instance, and only `DisposeService` will respond.

In addition, `DalamudPluginInterface` and `UiBuilder` also have been
changed so that their `IDisposable.Dispose` no longer respond, and
instead, internal functions have been added to only allow disposal from
Dalamud.

* Cleanup

* Postmerge fixes

* More explanation on RunOnFrameworkThread(ClearHooks)

* Mark ReliableFileStorage public ctor obsolete

---------

Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
2024-03-16 15:58:05 +00:00

1085 lines
40 KiB
C#

// #define IMEDEBUG
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using ImGuiNET;
#if IMEDEBUG
using Serilog;
#endif
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>
/// This class handles CJK IME.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudIme : IInternalDisposableService
{
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
private const int CImGuiStbTextUndoOffset = 0xB59C0;
private const int ImePageSize = 9;
private static readonly Dictionary<int, string> WmNames =
typeof(WM).GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.IsLiteral && !x.IsInitOnly && x.FieldType == typeof(int))
.Select(x => ((int)x.GetRawConstantValue()!, x.Name))
.DistinctBy(x => x.Item1)
.ToDictionary(x => x.Item1, x => x.Name);
private static readonly UnicodeRange[] HanRange =
{
UnicodeRanges.CjkRadicalsSupplement,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.CjkUnifiedIdeographsExtensionA,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkCompatibilityIdeographs,
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,
};
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>
StbTextMakeUndoReplace;
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void> StbTextUndo;
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
private readonly InterfaceManager interfaceManager;
private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate;
/// <summary>The candidates.</summary>
private readonly List<string> candidateStrings = new();
/// <summary>The selected imm component.</summary>
private string compositionString = string.Empty;
/// <summary>The cursor position in screen coordinates.</summary>
private Vector2 cursorScreenPos;
/// <summary>The associated viewport.</summary>
private ImGuiViewportPtr associatedViewport;
/// <summary>The index of the first imm candidate in relation to the full list.</summary>
private CANDIDATELIST immCandNative;
/// <summary>The partial conversion from-range.</summary>
private int partialConversionFrom;
/// <summary>The partial conversion to-range.</summary>
private int partialConversionTo;
/// <summary>The cursor offset in the composition string.</summary>
private int compositionCursorOffset;
/// <summary>The input mode icon from <see cref="SeIconChar"/>.</summary>
private char inputModeIcon;
/// <summary>Undo range for modifying the buffer while composition is in progress.</summary>
private (int Start, int End, int Cursor)? temporaryUndoSelection;
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<ProcessModule>()
.First(x => x.ModuleName == "cimgui.dll")
.BaseAddress;
}
catch
{
return;
}
StbTextMakeUndoReplace =
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>)
(cimgui + CImGuiStbTextCreateUndoOffset);
StbTextUndo =
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void>)
(cimgui + CImGuiStbTextUndoOffset);
}
[ServiceManager.ServiceConstructor]
private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws)
{
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
this.interfaceManager = imws.Manager;
this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData;
ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate);
this.interfaceManager.Draw += this.Draw;
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
}
/// <summary>
/// Finalizes an instance of the <see cref="DalamudIme"/> class.
/// </summary>
~DalamudIme() => this.ReleaseUnmanagedResources();
private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data);
/// <summary>
/// Gets a value indicating whether Han(Chinese) input has been detected.
/// </summary>
public bool EncounteredHan { get; private set; }
/// <summary>
/// Gets a value indicating whether Hangul(Korean) input has been detected.
/// </summary>
public bool EncounteredHangul { get; private set; }
/// <summary>
/// Gets a value indicating whether to display the cursor in input text. This also deals with blinking.
/// </summary>
private static bool ShowCursorInInputText
{
get
{
if (!ImGuiHelpers.IsImGuiInitialized)
return true;
if (!ImGui.GetIO().ConfigInputTextCursorBlink)
return true;
var textState = TextState;
if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0)
return true;
if (textState->CursorAnim <= 0)
return true;
return textState->CursorAnim % 1.2f <= 0.8f;
}
}
private static ImGuiInputTextState* TextState =>
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
/// <summary>Gets a value indicating whether to display partial conversion status.</summary>
private bool ShowPartialConversion => this.partialConversionFrom != 0 ||
this.partialConversionTo != this.compositionString.Length;
/// <summary>Gets a value indicating whether to draw.</summary>
private bool ShouldDraw =>
this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.interfaceManager.Draw -= this.Draw;
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <summary>
/// Looks for the characters inside <paramref name="str"/> and enables fonts accordingly.
/// </summary>
/// <param name="str">The string.</param>
public void ReflectCharacterEncounters(string str)
{
foreach (var chr in str)
{
if (!this.EncounteredHan)
{
if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
if (Service<FontAtlasFactory>.Get()
?.GetFdtReader(GameFontFamilyAndSize.Axis12)
.FindGlyph(chr) is null)
{
this.EncounteredHan = true;
Service<InterfaceManager>.Get().RebuildFonts();
}
}
}
if (!this.EncounteredHangul)
{
if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
this.EncounteredHangul = true;
Service<InterfaceManager>.Get().RebuildFonts();
}
}
}
}
private static string ImmGetCompositionString(HIMC hImc, uint comp)
{
var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0);
if (numBytes == 0)
return string.Empty;
var data = stackalloc char[numBytes / 2];
_ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes);
return new(data, 0, numBytes / 2);
}
private void ReleaseUnmanagedResources()
{
if (ImGuiHelpers.IsImGuiInitialized)
ImGui.GetIO().SetPlatformImeDataFn = nint.Zero;
}
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
{
if (!ImGuiHelpers.IsImGuiInitialized)
{
this.updateInputLanguage = true;
return;
}
// Are we not the target of text input?
if (!ImGui.GetIO().WantTextInput)
{
this.updateInputLanguage = true;
return;
}
var hImc = ImmGetContext(args.Hwnd);
if (hImc == nint.Zero)
{
this.updateInputLanguage = true;
return;
}
try
{
var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
#if IMEDEBUG
switch (args.Message)
{
case WM.WM_IME_NOTIFY:
Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({ImeDebug.ImnName((int)args.WParam)}, 0x{args.LParam:X})");
break;
case WM.WM_IME_CONTROL:
Log.Verbose(
$"{nameof(WM.WM_IME_CONTROL)}({ImeDebug.ImcName((int)args.WParam)}, 0x{args.LParam:X})");
break;
case WM.WM_IME_REQUEST:
Log.Verbose(
$"{nameof(WM.WM_IME_REQUEST)}({ImeDebug.ImrName((int)args.WParam)}, 0x{args.LParam:X})");
break;
case WM.WM_IME_SELECT:
Log.Verbose($"{nameof(WM.WM_IME_SELECT)}({(int)args.WParam != 0}, 0x{args.LParam:X})");
break;
case WM.WM_IME_STARTCOMPOSITION:
Log.Verbose($"{nameof(WM.WM_IME_STARTCOMPOSITION)}()");
break;
case WM.WM_IME_COMPOSITION:
Log.Verbose(
$"{nameof(WM.WM_IME_COMPOSITION)}({(char)args.WParam}, {ImeDebug.GcsName((int)args.LParam)})");
break;
case WM.WM_IME_COMPOSITIONFULL:
Log.Verbose($"{nameof(WM.WM_IME_COMPOSITIONFULL)}()");
break;
case WM.WM_IME_ENDCOMPOSITION:
Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}()");
break;
case WM.WM_IME_CHAR:
Log.Verbose($"{nameof(WM.WM_IME_CHAR)}({(char)args.WParam}, 0x{args.LParam:X})");
break;
case WM.WM_IME_KEYDOWN:
Log.Verbose($"{nameof(WM.WM_IME_KEYDOWN)}({(char)args.WParam}, 0x{args.LParam:X})");
break;
case WM.WM_IME_KEYUP:
Log.Verbose($"{nameof(WM.WM_IME_KEYUP)}({(char)args.WParam}, 0x{args.LParam:X})");
break;
case WM.WM_IME_SETCONTEXT:
Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(int)args.WParam != 0}, 0x{args.LParam:X})");
break;
}
#endif
if (this.updateInputLanguage
|| (args.Message == WM.WM_IME_NOTIFY
&& (int)args.WParam
is IMN.IMN_SETCONVERSIONMODE
or IMN.IMN_OPENSTATUSWINDOW
or IMN.IMN_CLOSESTATUSWINDOW))
{
this.UpdateInputLanguage(hImc);
this.updateInputLanguage = false;
}
if (this.updateImeStatusAgain)
{
this.ReplaceCompositionString(hImc, false);
this.UpdateCandidates(hImc);
this.updateImeStatusAgain = false;
}
switch (args.Message)
{
case WM.WM_IME_NOTIFY
when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE
or IMN.IMN_CHANGECANDIDATE:
this.UpdateCandidates(hImc);
this.updateImeStatusAgain = true;
args.SuppressWithValue(0);
break;
case WM.WM_IME_STARTCOMPOSITION:
this.updateImeStatusAgain = true;
args.SuppressWithValue(0);
break;
case WM.WM_IME_COMPOSITION:
if (invalidTarget)
ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
else
this.ReplaceCompositionString(hImc, ((int)args.LParam & GCS.GCS_RESULTSTR) != 0);
this.updateImeStatusAgain = true;
args.SuppressWithValue(0);
break;
case WM.WM_IME_ENDCOMPOSITION:
this.ClearState(hImc, false);
this.updateImeStatusAgain = true;
args.SuppressWithValue(0);
break;
case WM.WM_IME_CHAR:
case WM.WM_IME_KEYDOWN:
case WM.WM_IME_KEYUP:
case WM.WM_IME_CONTROL:
case WM.WM_IME_REQUEST:
this.updateImeStatusAgain = true;
args.SuppressWithValue(0);
break;
case WM.WM_IME_SETCONTEXT:
// Hide candidate and composition windows.
args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF));
this.updateImeStatusAgain = true;
args.SuppressWithDefault();
break;
case WM.WM_IME_NOTIFY:
case WM.WM_IME_COMPOSITIONFULL:
case WM.WM_IME_SELECT:
this.updateImeStatusAgain = true;
break;
case WM.WM_KEYDOWN when (int)args.WParam is
VK.VK_TAB
or VK.VK_PRIOR
or VK.VK_NEXT
or VK.VK_END
or VK.VK_HOME
or VK.VK_LEFT
or VK.VK_UP
or VK.VK_RIGHT
or VK.VK_DOWN
or VK.VK_RETURN:
if (this.candidateStrings.Count != 0)
{
this.ClearState(hImc);
args.WParam = VK.VK_PROCESSKEY;
}
this.UpdateCandidates(hImc);
break;
case WM.WM_KEYDOWN when (int)args.WParam is VK.VK_ESCAPE && this.candidateStrings.Count != 0:
this.ClearState(hImc);
args.SuppressWithDefault();
break;
case WM.WM_LBUTTONDOWN:
case WM.WM_RBUTTONDOWN:
case WM.WM_MBUTTONDOWN:
case WM.WM_XBUTTONDOWN:
ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
break;
}
}
finally
{
ImmReleaseContext(args.Hwnd, hImc);
}
}
private void UpdateInputLanguage(HIMC hImc)
{
uint conv, sent;
ImmGetConversionStatus(hImc, &conv, &sent);
var lang = GetKeyboardLayout(0);
var open = ImmGetOpenStatus(hImc) != false;
var native = (conv & 1) != 0;
var katakana = (conv & 2) != 0;
var fullwidth = (conv & 8) != 0;
switch (lang & 0x3F)
{
case LANG.LANG_KOREAN:
if (native)
this.inputModeIcon = (char)SeIconChar.ImeKoreanHangul;
else if (fullwidth)
this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric;
else
this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
break;
case LANG.LANG_JAPANESE:
// wtf
// see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0
if (open && native && katakana && fullwidth)
this.inputModeIcon = (char)SeIconChar.ImeKatakana;
else if (open && native && katakana)
this.inputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth;
else if (open && native)
this.inputModeIcon = (char)SeIconChar.ImeHiragana;
else if (open && fullwidth)
this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric;
else
this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
break;
case LANG.LANG_CHINESE:
if (native)
this.inputModeIcon = (char)SeIconChar.ImeChineseHan;
else
this.inputModeIcon = (char)SeIconChar.ImeChineseLatin;
break;
default:
this.inputModeIcon = default;
break;
}
}
private void ReplaceCompositionString(HIMC hImc, bool finalCommit)
{
var newString = finalCommit
? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR)
: ImmGetCompositionString(hImc, GCS.GCS_COMPSTR);
#if IMEDEBUG
Log.Verbose($"{nameof(this.ReplaceCompositionString)}({newString})");
#endif
this.ReflectCharacterEncounters(newString);
if (this.temporaryUndoSelection is not null)
{
TextState->Undo();
TextState->SelectionTuple = this.temporaryUndoSelection.Value;
this.temporaryUndoSelection = null;
}
TextState->SanitizeSelectionRange();
if (TextState->ReplaceSelectionAndPushUndo(newString))
this.temporaryUndoSelection = TextState->SelectionTuple;
// Put the cursor at the beginning, so that the candidate window appears aligned with the text.
TextState->SetSelectionRange(TextState->SelectionTuple.Start, newString.Length, 0);
if (finalCommit)
{
this.ClearState(hImc, false);
newString = string.Empty;
}
this.compositionString = newString;
this.compositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0);
var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0);
if (attrLength > 0)
{
var attrPtr = stackalloc byte[attrLength];
var attr = new Span<byte>(attrPtr, Math.Min(this.compositionString.Length, attrLength));
_ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength);
var l = 0;
while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED)
l++;
var r = l;
while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED)
r++;
if (r == 0 || l == this.compositionString.Length)
(l, r) = (0, this.compositionString.Length);
(this.partialConversionFrom, this.partialConversionTo) = (l, r);
}
else
{
this.partialConversionFrom = 0;
this.partialConversionTo = this.compositionString.Length;
}
this.UpdateCandidates(hImc);
}
private void ClearState(HIMC hImc, bool invokeCancel = true)
{
this.compositionString = string.Empty;
this.partialConversionFrom = this.partialConversionTo = 0;
this.compositionCursorOffset = 0;
this.temporaryUndoSelection = null;
TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd;
this.candidateStrings.Clear();
this.immCandNative = default;
if (invokeCancel)
ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
#if IMEDEBUG
Log.Information($"{nameof(this.ClearState)}({invokeCancel})");
#endif
}
private void UpdateCandidates(HIMC hImc)
{
this.candidateStrings.Clear();
this.immCandNative = default;
if (hImc == default)
return;
var size = (int)ImmGetCandidateListW(hImc, 0, null, 0);
if (size == 0)
return;
var pStorage = stackalloc byte[size];
if (size != ImmGetCandidateListW(hImc, 0, (CANDIDATELIST*)pStorage, (uint)size))
return;
ref var candlist = ref *(CANDIDATELIST*)pStorage;
this.immCandNative = candlist;
if (candlist.dwPageSize == 0 || candlist.dwCount == 0)
return;
foreach (var i in Enumerable.Range(
(int)candlist.dwPageStart,
(int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize)))
{
this.candidateStrings.Add(new((char*)(pStorage + candlist.dwOffset[i])));
this.ReflectCharacterEncounters(this.candidateStrings[^1]);
}
}
private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data)
{
this.cursorScreenPos = data.InputPos;
this.associatedViewport = data.WantVisible ? viewport : default;
}
private void Draw()
{
if (!this.ShouldDraw)
return;
if (Service<DalamudIme>.GetNullable() is not { } ime)
return;
var viewport = ime.associatedViewport;
if (viewport.NativePtr is null)
return;
var drawCand = ime.candidateStrings.Count != 0;
var drawConv = drawCand || ime.ShowPartialConversion;
var drawIme = ime.inputModeIcon != 0;
var imeIconFont = InterfaceManager.DefaultFont;
var pad = ImGui.GetStyle().WindowPadding;
var candTextSize = ImGui.CalcTextSize(ime.compositionString == string.Empty ? " " : ime.compositionString);
var native = ime.immCandNative;
var totalIndex = native.dwSelection + 1;
var totalSize = native.dwCount;
var pageStart = native.dwPageStart;
var pageIndex = (pageStart / ImePageSize) + 1;
var pageCount = (totalSize / ImePageSize) + 1;
var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})";
// Calc the window size.
var maxTextWidth = 0f;
for (var i = 0; i < ime.candidateStrings.Count; i++)
{
var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.candidateStrings[i]}");
maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X;
}
maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X;
maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.compositionString).X
? maxTextWidth
: ImGui.CalcTextSize(ime.compositionString).X;
var numEntries = (drawCand ? ime.candidateStrings.Count + 1 : 0) + 1 + (drawIme ? 1 : 0);
var spaceY = ImGui.GetStyle().ItemSpacing.Y;
var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries);
var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2);
// 1. Figure out the expanding direction.
var expandUpward = ime.cursorScreenPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y;
var windowPos = ime.cursorScreenPos - pad;
if (expandUpward)
{
windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2);
if (drawIme)
windowPos.Y += candTextSize.Y + spaceY;
}
else
{
if (drawIme)
windowPos.Y -= candTextSize.Y + spaceY;
}
// 2. Contain within the viewport. Do not use clamp, as the target window might be too small.
if (windowPos.X < viewport.WorkPos.X)
windowPos.X = viewport.WorkPos.X;
else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X)
windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X;
if (windowPos.Y < viewport.WorkPos.Y)
windowPos.Y = viewport.WorkPos.Y;
else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y)
windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y;
var cursor = windowPos + pad;
// Draw the ime window.
var drawList = ImGui.GetForegroundDrawList(viewport);
// Draw the background rect for candidates.
if (drawCand)
{
Vector2 candRectLt, candRectRb;
if (!expandUpward)
{
candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 };
candRectRb = windowPos + windowSize;
if (drawIme)
candRectLt.Y += spaceY + candTextSize.Y;
}
else
{
candRectLt = windowPos;
candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 });
if (drawIme)
candRectRb.Y -= spaceY + candTextSize.Y;
}
drawList.AddRectFilled(
candRectLt,
candRectRb,
ImGui.GetColorU32(ImGuiCol.WindowBg),
ImGui.GetStyle().WindowRounding);
}
if (!expandUpward && drawIme)
{
for (var dx = -2; dx <= 2; dx++)
{
for (var dy = -2; dy <= 2; dy++)
{
if (dx != 0 || dy != 0)
{
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor + new Vector2(dx, dy),
ImGui.GetColorU32(ImGuiCol.WindowBg),
ime.inputModeIcon);
}
}
}
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor,
ImGui.GetColorU32(ImGuiCol.Text),
ime.inputModeIcon);
cursor.Y += candTextSize.Y + spaceY;
}
if (!expandUpward && drawConv)
{
DrawTextBeingConverted();
cursor.Y += candTextSize.Y + spaceY;
// Add a separator.
drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
}
if (drawCand)
{
// Add the candidate words.
for (var i = 0; i < ime.candidateStrings.Count; i++)
{
var selected = i == (native.dwSelection % ImePageSize);
var color = ImGui.GetColorU32(ImGuiCol.Text);
if (selected)
color = ImGui.GetColorU32(ImGuiCol.NavHighlight);
drawList.AddText(cursor, color, $"{i + 1}. {ime.candidateStrings[i]}");
cursor.Y += candTextSize.Y + spaceY;
}
// Add a separator
drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
// Add the pages infomation.
drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo);
cursor.Y += candTextSize.Y + spaceY;
}
if (expandUpward && drawConv)
{
// Add a separator.
drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
DrawTextBeingConverted();
cursor.Y += candTextSize.Y + spaceY;
}
if (expandUpward && drawIme)
{
for (var dx = -2; dx <= 2; dx++)
{
for (var dy = -2; dy <= 2; dy++)
{
if (dx != 0 || dy != 0)
{
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor + new Vector2(dx, dy),
ImGui.GetColorU32(ImGuiCol.WindowBg),
ime.inputModeIcon);
}
}
}
imeIconFont.RenderChar(
drawList,
imeIconFont.FontSize,
cursor,
ImGui.GetColorU32(ImGuiCol.Text),
ime.inputModeIcon);
}
return;
void DrawTextBeingConverted()
{
// Draw the text background.
drawList.AddRectFilled(
cursor - (pad / 2),
cursor + candTextSize + (pad / 2),
ImGui.GetColorU32(ImGuiCol.WindowBg));
// If only a part of the full text is marked for conversion, then draw background for the part being edited.
if (ime.partialConversionFrom != 0 || ime.partialConversionTo != ime.compositionString.Length)
{
var part1 = ime.compositionString[..ime.partialConversionFrom];
var part2 = ime.compositionString[..ime.partialConversionTo];
var size1 = ImGui.CalcTextSize(part1);
var size2 = ImGui.CalcTextSize(part2);
drawList.AddRectFilled(
cursor + size1 with { Y = 0 },
cursor + size2,
ImGui.GetColorU32(ImGuiCol.TextSelectedBg));
}
// Add the text being converted.
drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.compositionString);
// Draw the caret inside the composition string.
if (DalamudIme.ShowCursorInInputText)
{
var partBeforeCaret = ime.compositionString[..ime.compositionCursorOffset];
var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret);
drawList.AddLine(
cursor + sizeBeforeCaret with { Y = 0 },
cursor + sizeBeforeCaret,
ImGui.GetColorU32(ImGuiCol.Text));
}
}
}
/// <summary>
/// Ported from imstb_textedit.h.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE2C)]
private struct StbTextEditState
{
/// <summary>
/// Position of the text cursor within the string.
/// </summary>
public int Cursor;
/// <summary>
/// Selection start point.
/// </summary>
public int SelectStart;
/// <summary>
/// selection start and end point in characters; if equal, no selection.
/// </summary>
/// <remarks>
/// 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.)
/// </remarks>
public int SelectEnd;
/// <summary>
/// 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.
/// </summary>
public byte InsertMode;
/// <summary>
/// Page size in number of row.
/// This value MUST be set to >0 for pageup or pagedown in multilines documents.
/// </summary>
public int RowCountPerPage;
// Remainder is stb-private data.
}
[StructLayout(LayoutKind.Sequential)]
private struct ImGuiInputTextState
{
public uint Id;
public int CurLenW;
public int CurLenA;
public ImVector<char> TextWRaw;
public ImVector<byte> TextARaw;
public ImVector<byte> 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<char> TextW => new((ImVector*)&this.ThisPtr->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 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() => StbTextUndo(this.ThisPtr, &this.ThisPtr->Stb);
public bool MakeUndoReplace(int offset, int oldLength, int newLength)
{
if (oldLength == 0 && newLength == 0)
return false;
StbTextMakeUndoReplace(this.ThisPtr, &this.ThisPtr->Stb, offset, oldLength, newLength);
return true;
}
public bool ReplaceSelectionAndPushUndo(ReadOnlySpan<char> 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<char> 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<char> 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
{
private static readonly (int Value, string Name)[] GcsFields =
{
(GCS.GCS_COMPREADSTR, nameof(GCS.GCS_COMPREADSTR)),
(GCS.GCS_COMPREADATTR, nameof(GCS.GCS_COMPREADATTR)),
(GCS.GCS_COMPREADCLAUSE, nameof(GCS.GCS_COMPREADCLAUSE)),
(GCS.GCS_COMPSTR, nameof(GCS.GCS_COMPSTR)),
(GCS.GCS_COMPATTR, nameof(GCS.GCS_COMPATTR)),
(GCS.GCS_COMPCLAUSE, nameof(GCS.GCS_COMPCLAUSE)),
(GCS.GCS_CURSORPOS, nameof(GCS.GCS_CURSORPOS)),
(GCS.GCS_DELTASTART, nameof(GCS.GCS_DELTASTART)),
(GCS.GCS_RESULTREADSTR, nameof(GCS.GCS_RESULTREADSTR)),
(GCS.GCS_RESULTREADCLAUSE, nameof(GCS.GCS_RESULTREADCLAUSE)),
(GCS.GCS_RESULTSTR, nameof(GCS.GCS_RESULTSTR)),
(GCS.GCS_RESULTCLAUSE, nameof(GCS.GCS_RESULTCLAUSE)),
};
private static readonly IReadOnlyDictionary<int, string> ImnFields =
typeof(IMN)
.GetFields(BindingFlags.Static | BindingFlags.Public)
.Where(x => x.IsLiteral)
.ToDictionary(x => (int)x.GetRawConstantValue()!, x => x.Name);
public static string GcsName(int val)
{
var sb = new StringBuilder();
foreach (var (value, name) in GcsFields)
{
if ((val & value) != 0)
{
if (sb.Length != 0)
sb.Append(" | ");
sb.Append(name);
val &= ~value;
}
}
if (val != 0)
{
if (sb.Length != 0)
sb.Append(" | ");
sb.Append($"0x{val:X}");
}
return sb.ToString();
}
public static string ImcName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}";
public static string ImnName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}";
public static string ImrName(int val) => val switch
{
IMR_CANDIDATEWINDOW => nameof(IMR_CANDIDATEWINDOW),
IMR_COMPOSITIONFONT => nameof(IMR_COMPOSITIONFONT),
IMR_COMPOSITIONWINDOW => nameof(IMR_COMPOSITIONWINDOW),
IMR_CONFIRMRECONVERTSTRING => nameof(IMR_CONFIRMRECONVERTSTRING),
IMR_DOCUMENTFEED => nameof(IMR_DOCUMENTFEED),
IMR_QUERYCHARPOSITION => nameof(IMR_QUERYCHARPOSITION),
IMR_RECONVERTSTRING => nameof(IMR_RECONVERTSTRING),
_ => $"0x{val:X}",
};
}
#endif
}