// F07 — IME / text input, Win32 implementation
// (../../../features/f07-text-input.md). Extends the scaffold
// (../scaffold/app.d) into an IME observatory with two layers:
//
// * TSF probe (the spec-mandated decision record): a minimal COM bring-up of
// the Text Services Framework from plain D — CoInitializeEx ->
// CoCreateInstance(CLSID_TF_ThreadMgr) -> ITfThreadMgr.Activate ->
// CreateDocumentMgr -> CreateContext(punk=null) -> Push -> AssociateFocus.
// druntime's core.sys.windows has no msctf.h projection, so the two vtbls
// and three GUIDs are hand-declared below. The probe logs every HRESULT
// and a tsf_verdict, then tears everything down: real TSF text input
// requires the app to implement ITextStoreACP (~30 methods) + sink
// interfaces, which is the documented reason the demo's editor speaks
// IMM32 (see the findings doc, ../../f07-text-input.md).
// * IMM32 editor (implemented fully): an editable line with a caret
// (CreateCaret/SetCaretPos), the WM_IME_STARTCOMPOSITION ->
// WM_IME_COMPOSITION (ImmGetCompositionStringW GCS_COMPSTR + GCS_COMPATTR
// + GCS_CURSORPOS, GCS_RESULTSTR) -> WM_IME_ENDCOMPOSITION choreography,
// inline underlined pre-edit rendering, and ImmSetCandidateWindow
// (CANDIDATEFORM CFS_EXCLUDE anchored at the caret, re-set on every caret
// move). WM_IME_SETCONTEXT / WM_IME_NOTIFY / ImmGetDefaultIMEWnd are
// logged; an ImmAssociateContext(null) probe disables and restores the IME.
//
// Under Wine without a real IME the composition messages cannot fire from
// typing, so a WSI_AUTO_EXIT=1 script probes what imm32 provides: typed ASCII
// (caret + candidate-anchor movement), an ImmSetCompositionStringW(SCS_SETSTR)
// self-injection, NI_COMPOSITIONSTR commit/cancel, and the associate-null
// round-trip. The full CJK choreography is the Tier-C manual script in the
// findings doc.
//
// Only druntime's built-in core.sys.windows bindings — no third-party packages.
module (module) appapp;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.windowsWindows API header module
Translated from MinGW API for MS-Windows 4.0
Source
core/sys/windows/windows.d
windows;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.immWindows API header module
Translated from MinGW Windows headers
Source
core/sys/windows/imm.d
imm;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.objbaseWindows API header module
Translated from MinGW Windows headers
Source
core/sys/windows/objbase.d
objbase : CoInitializeEx, CoUninitialize, CoCreateInstance, COINIT;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.wtypesWindows API header module
Translated from MinGW Windows headers
Source
core/sys/windows/wtypes.d
wtypes : CLSCTX;
import instrument;
pragma(lib, "imm32");
pragma(lib, "ole32");
// ---------------------------------------------------------------------------
// druntime's core.sys.windows.imm declares `alias DWORD HIMC` — but the real
// HIMC is a pointer-sized DECLARE_HANDLE, so on Win64 those prototypes
// truncate the handle (and none are nothrow, which a WndProc needs). The
// constants/structs (WM_IME_*, GCS_*, CFS_*, CANDIDATEFORM, …) are fine;
// the used function surface is redeclared correctly here (module-level
// declarations take precedence over imports). A finding in its own right —
// see ../../f07-text-input.md.
alias (alias) app.HIMC = _error_HIMC = HANDLE;
extern (Windows) nothrow @nogc
{
(alias) app.HIMC = _error_HIMC app.ImmGetContextImmGetContext(HWND);
BOOL app.ImmReleaseContextImmReleaseContext(HWND, (alias) app.HIMC = _error_HIMC);
(alias) app.HIMC = _error_HIMC app.ImmAssociateContextImmAssociateContext(HWND, (alias) app.HIMC = _error_HIMC);
HWND app.ImmGetDefaultIMEWndImmGetDefaultIMEWnd(HWND);
BOOL app.ImmGetOpenStatusImmGetOpenStatus((alias) app.HIMC = _error_HIMC);
BOOL app.ImmGetConversionStatusImmGetConversionStatus((alias) app.HIMC = _error_HIMC, LPDWORD, LPDWORD);
UINT app.ImmGetDescriptionWImmGetDescriptionW(HKL, LPWSTR, UINT);
LONG app.ImmGetCompositionStringWImmGetCompositionStringW((alias) app.HIMC = _error_HIMC, DWORD, PVOID, DWORD);
BOOL app.ImmSetCompositionStringWImmSetCompositionStringW((alias) app.HIMC = _error_HIMC, DWORD, PCVOID, DWORD, PCVOID, DWORD);
BOOL app.ImmNotifyIMEImmNotifyIME((alias) app.HIMC = _error_HIMC, DWORD, DWORD, DWORD);
BOOL app.ImmSetCandidateWindowImmSetCandidateWindow((alias) app.HIMC = _error_HIMC, PCANDIDATEFORM);
BOOL app.ImmSetCompositionWindowImmSetCompositionWindow((alias) app.HIMC = _error_HIMC, PCOMPOSITIONFORM);
}
// ---------------------------------------------------------------------------
// TSF surface — hand-declared. druntime has IUnknown but nothing from msctf.h;
// these are the exact vtbl orders of the msctf.h interfaces (Windows SDK).
alias (alias) app.TfClientId = _error_TfClientId = DWORD;
alias (alias) app.TfEditCookie = _error_TfEditCookie = DWORD;
enum (constant) int app.TF_POPF_ALL = 1TF_POPF_ALL = 0x1;
interface (interface) app.ITfContextITfContext : IUnknown {} // opaque here — no method is called on it
interface (interface) app.ITfDocumentMgrITfDocumentMgr : IUnknown
{
extern (Windows) nothrow:
HRESULT app.ITfDocumentMgr.CreateContextCreateContext((alias) app.TfClientId = _error_TfClientId tidOwner, DWORD dwFlags, IUnknown punk,
(interface) app.ITfContextITfContext* ppic, (alias) app.TfEditCookie = _error_TfEditCookie* pecTextStore);
HRESULT app.ITfDocumentMgr.PushPush((interface) app.ITfContextITfContext pic);
HRESULT app.ITfDocumentMgr.PopPop(DWORD dwFlags);
HRESULT app.ITfDocumentMgr.GetTopGetTop((interface) app.ITfContextITfContext* ppic);
HRESULT app.ITfDocumentMgr.GetBaseGetBase((interface) app.ITfContextITfContext* ppic);
HRESULT app.ITfDocumentMgr.EnumContextsEnumContexts(void** ppEnum);
}
interface (interface) app.ITfThreadMgrITfThreadMgr : IUnknown
{
extern (Windows) nothrow:
HRESULT app.ITfThreadMgr.ActivateActivate((alias) app.TfClientId = _error_TfClientId* ptid);
HRESULT app.ITfThreadMgr.DeactivateDeactivate();
HRESULT app.ITfThreadMgr.CreateDocumentMgrCreateDocumentMgr((interface) app.ITfDocumentMgrITfDocumentMgr* ppdim);
HRESULT app.ITfThreadMgr.EnumDocumentMgrsEnumDocumentMgrs(void** ppEnum);
HRESULT app.ITfThreadMgr.GetFocusGetFocus((interface) app.ITfDocumentMgrITfDocumentMgr* ppdimFocus);
HRESULT app.ITfThreadMgr.SetFocusSetFocus((interface) app.ITfDocumentMgrITfDocumentMgr pdimFocus);
HRESULT app.ITfThreadMgr.AssociateFocusAssociateFocus(HWND (parameter) HWND hwndhwnd, (interface) app.ITfDocumentMgrITfDocumentMgr pdimNew, (interface) app.ITfDocumentMgrITfDocumentMgr* ppdimPrev);
HRESULT app.ITfThreadMgr.IsThreadFocusIsThreadFocus(BOOL* pfThreadFocus);
HRESULT app.ITfThreadMgr.GetFunctionProviderGetFunctionProvider(REFCLSID clsid, void** ppFuncProv);
HRESULT app.ITfThreadMgr.EnumFunctionProvidersEnumFunctionProviders(void** ppEnum);
HRESULT app.ITfThreadMgr.GetGlobalCompartmentGetGlobalCompartment(void** ppCompMgr);
}
immutable GUID (immutable global) immutable(_error_) app.CLSID_TF_ThreadMgrCLSID_TF_ThreadMgr =
{0x529a9e6b, 0x6587, 0x4f23, [0xab, 0x9e, 0x9c, 0x7d, 0x68, 0x3e, 0x3c, 0x50]};
immutable GUID (immutable global) immutable(_error_) app.IID_ITfThreadMgrIID_ITfThreadMgr =
{0xaa80e801, 0x2021, 0x11d2, [0x93, 0xe0, 0x00, 0x60, 0xb0, 0x67, 0xb8, 0x6e]};
// The minimal bring-up: how far plain D gets into TSF without ITextStoreACP.
// Every step logs its HRESULT; the verdict line records where it stopped.
// (Not nothrow: IUnknown.Release in druntime is a throwing prototype. The
// probe runs from main, never from the WndProc, so that is acceptable.)
void app.tsfProbetsfProbe(HWND (parameter) HWND hwndhwnd)
{
auto _error_ hrhr = CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
logEvent("tsf step=CoInitializeEx hr=0x%08lx", hr);
(unresolved type) ITfThreadMgrITfThreadMgr _error_ tmtm;
hr = CoCreateInstance(&CLSID_TF_ThreadMgr, null, CLSCTX.CLSCTX_INPROC_SERVER,
&IID_ITfThreadMgr, cast(void**)&tm);
logEvent("tsf step=CoCreateInstance clsid=TF_ThreadMgr hr=0x%08lx ptr=%d",
hr, tm !is null ? 1 : 0);
if (tm is null)
{
logEvent("tsf_verdict reached=none note=thread_mgr_unavailable");
return;
}
(unresolved type) TfClientIdTfClientId _error_ tidtid;
hr = tm.tm.ActivateActivate(&tid);
logEvent("tsf step=Activate hr=0x%08lx client_id=0x%lx", hr, tid);
(unresolved type) ITfDocumentMgrITfDocumentMgr _error_ dmdm;
hr = tm.tm.CreateDocumentMgrCreateDocumentMgr(&dm);
logEvent("tsf step=CreateDocumentMgr hr=0x%08lx ptr=%d", hr, dm !is null ? 1 : 0);
(unresolved type) ITfContextITfContext _error_ icic;
(unresolved type) TfEditCookieTfEditCookie _error_ cookiecookie;
if (dm !is null)
{
// punk=null is allowed ("the context does not have a text store") but
// an IME can then neither read nor edit the document — the exact gap
// a real integration fills by implementing ITextStoreACP.
hr = dm.dm.CreateContextCreateContext(tid, 0, null, &ic, &cookie);
logEvent("tsf step=CreateContext punk=null hr=0x%08lx cookie=0x%lx", hr, cookie);
if (ic !is null)
{
hr = dm.dm.PushPush(ic);
logEvent("tsf step=Push hr=0x%08lx", hr);
}
(unresolved type) ITfDocumentMgrITfDocumentMgr _error_ prevprev;
hr = tm.tm.AssociateFocusAssociateFocus(hwnd, dm, &prev);
logEvent("tsf step=AssociateFocus hr=0x%08lx prev=%d", hr, prev !is null ? 1 : 0);
if (prev !is null)
prev.Release();
}
logEvent("tsf_verdict reached=%s note=no_ITextStoreACP_so_no_editable_store",
ic !is null ? "context_pushed_and_focused".ptr : "thread_mgr_only".ptr);
// Full teardown so the IMM32 half below runs on a clean thread.
(unresolved type) ITfDocumentMgrITfDocumentMgr _error_ prev2prev2;
tm.tm.AssociateFocusAssociateFocus(hwnd, null, &prev2);
if (prev2 !is null)
prev2.Release();
if (dm !is null)
dm.dm.PopPop(TF_POPF_ALL);
if (ic !is null)
ic.Release();
if (dm !is null)
dm.Release();
hr = tm.tm.DeactivateDeactivate();
logEvent("tsf step=Deactivate hr=0x%08lx", hr);
tm.Release();
}
// ---------------------------------------------------------------------------
// Editor state. Committed text + caret index, plus the live pre-edit run that
// is rendered inline (underlined) at the caret during a composition.
enum (constant) int app.MARGIN = 12MARGIN = 12; // text origin inside the client area
enum UINT_PTR (constant) _error_ app.TIMER_ID = __errorTIMER_ID = 1;
enum (constant) int app.TICK_MS = 150TICK_MS = 150; // one script step per tick
struct (struct) app.DemoDemo
{
HWND (field) _error_ app.Demo.hwndhwnd;
(alias) app.HIMC = _error_HIMC (field) _error_ app.Demo.himcSavedhimcSaved; // != null while the associate-null probe is active
wchar[256] (field) wchar[256] app.Demo.texttext; // committed text
int (field) int app.Demo.lenlen, (field) int app.Demo.caretcaret; // committed length / caret index into text[]
wchar[64] (field) wchar[64] app.Demo.preeditpreedit; // live composition string (GCS_COMPSTR)
int (field) int app.Demo.preLenpreLen, (field) int app.Demo.preCursorpreCursor; // pre-edit length / cursor (GCS_CURSORPOS)
int (field) int app.Demo.lineHeightlineHeight = 16;
bool (field) bool app.Demo.composingcomposing, (field) bool app.Demo.autoExitautoExit, (field) bool app.Demo.focusedfocused;
uint (field) uint app.Demo.stepstep;
uint (field) uint app.Demo.nStartnStart, (field) uint app.Demo.nCompnComp, (field) uint app.Demo.nEndnEnd, (field) uint app.Demo.nSetCtxnSetCtx, (field) uint app.Demo.nNotifynNotify, (field) uint app.Demo.nImeCharnImeChar, (field) uint app.Demo.nCharnChar, (field) uint app.Demo.nCommitnCommit;
}
__gshared (struct) app.DemoDemo _error_ app.gg;
void void app.utf8z(scope const(wchar)[] s, scope char[] o) nothrow @nogcutf8z(scope const(wchar)[] (parameter) const(wchar)[] ss, scope char[] (parameter) char[] oo) nothrow @nogc
{
(alias) object.size_t = ulongsize_t (local variable) ulong nn;
foreach ((parameter) ulong ii, wchar (parameter) wchar uu; (parameter) const(wchar)[] ss) // unpaired-surrogate-naive: fine for log lines
{
dchar (local variable) dchar cc = (local variable) wchar uu;
if ((local variable) wchar uu >= 0xd800 && (local variable) wchar uu <= 0xdbff && (local variable) ulong ii + 1 < (parameter) const(wchar)[] ss.(field) ulong const(wchar)[].lengthlength)
continue; // high unit: emit at the low unit
if ((local variable) wchar uu >= 0xdc00 && (local variable) wchar uu <= 0xdfff && (local variable) ulong ii > 0)
(local variable) dchar cc = 0x10000 + (((parameter) const(wchar)[] ss[(local variable) ulong ii - 1] - 0xd800) << 10) + ((local variable) wchar uu - 0xdc00);
if ((local variable) ulong nn + 5 > (parameter) char[] oo.(field) ulong char[].lengthlength)
break;
if ((local variable) dchar cc < 0x80)
(parameter) char[] oo[(local variable) ulong nn++] = cast(char) (local variable) dchar cc;
else if ((local variable) dchar cc < 0x800)
{
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0xc0 | ((local variable) dchar cc >> 6));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | ((local variable) dchar cc & 0x3f));
}
else if ((local variable) dchar cc < 0x10000)
{
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0xe0 | ((local variable) dchar cc >> 12));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | (((local variable) dchar cc >> 6) & 0x3f));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | ((local variable) dchar cc & 0x3f));
}
else
{
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0xf0 | ((local variable) dchar cc >> 18));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | (((local variable) dchar cc >> 12) & 0x3f));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | (((local variable) dchar cc >> 6) & 0x3f));
(parameter) char[] oo[(local variable) ulong nn++] = cast(char)(0x80 | ((local variable) dchar cc & 0x3f));
}
}
(parameter) char[] oo[(local variable) ulong nn] = 0;
}
// Pixel x of the caret = extent of committed-before-caret + pre-edit-cursor.
int app.caretXcaretX(HDC (parameter) HDC hdchdc) nothrow
{
SIZE _error_ szsz;
int _error_ xx = MARGIN;
if (g.g.caretcaret > 0 && GetTextExtentPoint32W(hdc, g.g.texttext.ptr, g.g.caretcaret, &sz))
x += sz.cx;
if (g.g.composingcomposing && g.g.preCursorpreCursor > 0
&& GetTextExtentPoint32W(hdc, g.g.preeditpreedit.ptr, g.g.preCursorpreCursor, &sz))
x += sz.cx;
return x;
}
// The candidate-window anchoring contract: tell the IME where the caret is so
// the candidate list opens beside it and never covers the composition. The
// CFS_EXCLUDE rect is the caret line; re-sent on EVERY caret move.
void void app.anchorCandidateWindow() nothrowanchorCandidateWindow() nothrow
{
(alias) app.HIMC = _error_HIMC (local variable) _error_ himchimc = ImmGetContext(g.g.hwndhwnd);
if (himc is null)
{
logEvent("candidate_anchor skipped=no_himc");
return;
}
HDC (local variable) _error_ hdchdc = GetDC(g.g.hwndhwnd);
HGDIOBJ (local variable) _error_ oldold = SelectObject(hdc, GetStockObject(DEFAULT_GUI_FONT));
const (local variable) const(_error_) xx = caretX(hdc);
SelectObject(hdc, old);
ReleaseDC(g.g.hwndhwnd, hdc);
CANDIDATEFORM (local variable) _error_ cfcf;
cf.dwIndex = 0;
cf.dwStyle = CFS_EXCLUDE;
cf.ptCurrentPos = POINT(x, MARGIN + g.g.lineHeightlineHeight);
cf.rcArea = RECT(x, MARGIN, x + 2, MARGIN + g.g.lineHeightlineHeight);
const (local variable) const(_error_) okok = ImmSetCandidateWindow(himc, &cf);
// Keep the composition window (the IME's inline-fallback UI) at the caret
// too — IMEs that ignore CANDIDATEFORM still honor COMPOSITIONFORM.
COMPOSITIONFORM (local variable) _error_ compcomp;
comp.dwStyle = CFS_POINT;
comp.ptCurrentPos = POINT(x, MARGIN);
const (local variable) const(_error_) ok2ok2 = ImmSetCompositionWindow(himc, &comp);
ImmReleaseContext(g.g.hwndhwnd, himc);
logEvent("candidate_anchor x=%d y=%d style=CFS_EXCLUDE ok=%d comp_ok=%d",
x, MARGIN + g.g.lineHeightlineHeight, ok ? 1 : 0, ok2 ? 1 : 0);
}
void void app.moveCaret(int delta) nothrowmoveCaret(int (parameter) int deltadelta) nothrow
{
const (local variable) const(_error_) nextnext = g.(field) _error_ g.caretcaret + (parameter) int deltadelta;
if (next < 0 || next > g.(field) _error_ g.lenlen)
return;
g.g.caretcaret = next;
logEvent("caret index=%d", g.g.caretcaret);
InvalidateRect(g.g.hwndhwnd, null, TRUE);
void app.anchorCandidateWindow() nothrowanchorCandidateWindow(); // the caret moved -> re-report the rectangle
}
void void app.insertText(scope const(wchar)[] s) nothrowinsertText(scope const(wchar)[] (parameter) const(wchar)[] ss) nothrow
{
foreach ((parameter) const(wchar) uu; (parameter) const(wchar)[] ss)
{
if (g.(field) _error_ g.lenlen >= g.(field) _error_ g.texttext.(field) _error_ g.text.lengthlength)
break;
foreach_reverse ((parameter) _error_ ii; g.(field) _error_ g.caretcaret .. g.(field) _error_ g.lenlen)
g.g.texttext[i + 1] = g.g.texttext[i];
g.(field) _error_ g.texttext[g.g.caretcaret++] = u;
g.(field) _error_ g.lenlen++;
}
InvalidateRect(g.g.hwndhwnd, null, TRUE);
void app.anchorCandidateWindow() nothrowanchorCandidateWindow();
}
// ---------------------------------------------------------------------------
// WM_IME_COMPOSITION payload readers.
int app.readImeStringreadImeString((alias) app.HIMC = _error_HIMC (parameter) HIMC himchimc, DWORD what, scope wchar[] (parameter) wchar[] oo) nothrow
{
const _error_ nn = ImmGetCompositionStringW(himc, what, o.ptr, cast(DWORD)(o.o.lengthlength * 2));
return n > 0 ? n / 2 : 0; // byte count -> wchar count (negative = error)
}
void app.onImeCompositiononImeComposition(HWND (parameter) HWND hwndhwnd, LPARAM lParam) nothrow
{
++g.g.nCompnComp;
logEvent("msg name=WM_IME_COMPOSITION flags=0x%llx", cast(ulong) lParam);
(unresolved type) HIMCHIMC _error_ himchimc = ImmGetContext(hwnd);
if (himc is null)
return;
if (lParam & GCS_RESULTSTR) // committed text replaces the pre-edit
{
wchar[64] _error_ rr;
const _error_ nn = readImeString(himc, GCS_RESULTSTR, r);
char[256] _error_ u8u8;
utf8z(r[0 .. n], u8);
logEvent("commit text=%s units=%d", u8.ptr, n);
++g.g.nCommitnCommit;
g.g.preLenpreLen = 0;
insertText(r[0 .. n]);
}
if (lParam & GCS_COMPSTR) // the live pre-edit + its attributes + cursor
{
g.g.preLenpreLen = readImeString(himc, GCS_COMPSTR, g.g.preeditpreedit);
ubyte[64] _error_ attrattr;
const _error_ anan = ImmGetCompositionStringW(himc, GCS_COMPATTR, attr.ptr, attr.attr.lengthlength);
const _error_ curcur = ImmGetCompositionStringW(himc, GCS_CURSORPOS, null, 0);
g.g.preCursorpreCursor = cur >= 0 ? cur : 0;
char[256] _error_ u8u8;
utf8z(g.g.preeditpreedit[0 .. g.preLen], u8);
char[80] _error_ a8a8;
(unresolved type) size_tsize_t _error_ aiai;
foreach ((parameter) ii; 0 .. (an > 0 && an < 40 ? an : 0)) // ATTR_* byte per wchar
a8[ai++] = cast(char)('0' + (attr[i] % 10));
a8[ai] = 0;
logEvent("preedit text=%s units=%d cursor=%d attr=%s",
u8.ptr, g.g.preLenpreLen, g.g.preCursorpreCursor, ai ? a8.ptr : "-".ptr);
}
if (lParam == 0) // composition canceled: clear the pre-edit
{
g.g.preLenpreLen = g.g.preCursorpreCursor = 0;
logEvent("preedit_cleared reason=flags_zero");
}
ImmReleaseContext(hwnd, himc);
InvalidateRect(hwnd, null, TRUE);
anchorCandidateWindow();
}
// ---------------------------------------------------------------------------
// Painting: committed text, then the pre-edit inline at the caret with a 1-px
// underline (the visual contract: pre-edit must be distinct from committed).
void app.paintpaint(HWND (parameter) HWND hwndhwnd) nothrow
{
PAINTSTRUCT _error_ psps;
HDC _error_ hdchdc = BeginPaint(hwnd, &ps);
RECT _error_ rcrc;
GetClientRect(hwnd, &rc);
FillRect(hdc, &rc, cast(HBRUSH)(COLOR_WINDOW + 1));
HGDIOBJ _error_ oldFontoldFont = SelectObject(hdc, GetStockObject(DEFAULT_GUI_FONT));
SetBkMode(hdc, TRANSPARENT);
TEXTMETRICW _error_ tmtm;
GetTextMetricsW(hdc, &tm);
g.g.lineHeightlineHeight = tm.tmHeight;
int _error_ xx = MARGIN;
SIZE _error_ szsz;
TextOutW(hdc, x, MARGIN, g.g.texttext.ptr, g.g.caretcaret); // committed, before caret
if (g.g.caretcaret && GetTextExtentPoint32W(hdc, g.g.texttext.ptr, g.g.caretcaret, &sz))
x += sz.cx;
if (g.g.preLenpreLen) // the pre-edit run, underlined
{
TextOutW(hdc, x, MARGIN, g.g.preeditpreedit.ptr, g.g.preLenpreLen);
GetTextExtentPoint32W(hdc, g.g.preeditpreedit.ptr, g.g.preLenpreLen, &sz);
HPEN _error_ penpen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
HGDIOBJ _error_ oldPenoldPen = SelectObject(hdc, pen);
MoveToEx(hdc, x, MARGIN + g.g.lineHeightlineHeight + 1, null);
LineTo(hdc, x + sz.cx, MARGIN + g.g.lineHeightlineHeight + 1);
SelectObject(hdc, oldPen);
DeleteObject(pen);
x += sz.cx;
}
TextOutW(hdc, x, MARGIN, g.g.texttext.ptr + g.g.caretcaret, g.g.lenlen - g.g.caretcaret); // after caret
if (g.g.focusedfocused)
SetCaretPos(caretX(hdc), MARGIN);
SelectObject(hdc, oldFont);
EndPaint(hwnd, &ps);
}
// ---------------------------------------------------------------------------
// SendInput at scancode level (same shape as ../f06-keyboard/app.d).
void void app.injectScans(scope const(ushort)[] seq) nothrowinjectScans(scope const(ushort)[] (parameter) const(ushort)[] seqseq) nothrow
{
INPUT[8] (local variable) _error_ inpinp;
foreach ((parameter) ulong ii, (parameter) const(ushort) ee; (parameter) const(ushort)[] seqseq)
{
inp[i].type = INPUT_KEYBOARD;
inp[i].ki.wScan = e & 0xff;
inp[i].ki.dwFlags = KEYEVENTF_SCANCODE | ((e & 0x8000) ? KEYEVENTF_KEYUP : 0);
}
const (local variable) const(_error_) sentsent = SendInput(cast(UINT) seq.seq.lengthlength, inp.ptr, INPUT.sizeof);
logEvent("inject kind=scancode events=%d sent=%u", cast(int) seq.seq.lengthlength, sent);
}
// ---------------------------------------------------------------------------
// The script: what an IME-less host (Wine headless) lets us prove, one step
// per timer tick. Steps 2-4 are the SCS_SETSTR/NI_COMPOSITIONSTR self-
// injection probe — if imm32 echoes WM_IME_COMPOSITION back, the choreography
// handlers above capture it.
void app.runSteprunStep(HWND (parameter) HWND hwndhwnd) nothrow
{
switch (g.g.stepstep++)
{
case 0:
logEvent("script step=probe_imm");
(unresolved type) HIMCHIMC _error_ himchimc = ImmGetContext(hwnd);
logEvent("imm context himc=0x%zx", cast((unresolved type) size_tsize_t) himc);
logEvent("imm default_ime_wnd hwnd=0x%zx",
cast((unresolved type) size_tsize_t) ImmGetDefaultIMEWnd(hwnd));
logEvent("imm open_status open=%d", himc ? (ImmGetOpenStatus(himc) ? 1 : 0) : -1);
DWORD _error_ convconv, _error_ sentsent;
const _error_ cscs = himc ? ImmGetConversionStatus(himc, &conv, &sent) : FALSE;
logEvent("imm conversion_status ok=%d conversion=0x%lx sentence=0x%lx",
cs ? 1 : 0, conv, sent);
WCHAR[64] _error_ descdesc;
const _error_ dndn = ImmGetDescriptionW(GetKeyboardLayout(0), desc.ptr, desc.desc.lengthlength);
logEvent("imm description len=%u note=%s", dn,
dn ? "ime_layout".ptr : "not_an_ime_layout".ptr);
if (himc)
ImmReleaseContext(hwnd, himc);
break;
case 1: // plain typing: caret moves, candidate anchor re-sent per char
logEvent("script step=type_ascii text=ab");
injectScans([0x1e, 0x1e | 0x8000, 0x30, 0x30 | 0x8000]); // a, b
break;
case 2: // self-injection: ask the IME to set the composition string
logEvent("script step=set_composition_string text=nihao");
(unresolved type) HIMCHIMC _error_ himchimc = ImmGetContext(hwnd);
auto _error_ ss = "nihao"w;
const _error_ okok = ImmSetCompositionStringW(himc, SCS_SETSTR,
cast(void*) s.ptr, cast(DWORD)(s.s.lengthlength * 2), null, 0);
logEvent("imm set_composition_string scs=SCS_SETSTR ok=%d err=%lu",
ok ? 1 : 0, ok ? 0 : GetLastError());
ImmReleaseContext(hwnd, himc);
break;
case 3: // commit whatever composition exists
logEvent("script step=notify_complete");
(unresolved type) HIMCHIMC _error_ himchimc = ImmGetContext(hwnd);
const _error_ okok = ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
logEvent("imm notify_ime action=CPS_COMPLETE ok=%d", ok ? 1 : 0);
ImmReleaseContext(hwnd, himc);
break;
case 4: // and the cancel path
logEvent("script step=notify_cancel");
(unresolved type) HIMCHIMC _error_ himchimc = ImmGetContext(hwnd);
auto _error_ ss = "x"w;
ImmSetCompositionStringW(himc, SCS_SETSTR, cast(void*) s.ptr, 2, null, 0);
const _error_ okok = ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_CANCEL, 0);
logEvent("imm notify_ime action=CPS_CANCEL ok=%d", ok ? 1 : 0);
ImmReleaseContext(hwnd, himc);
break;
case 5: // IME disable: ImmAssociateContext(null) detaches the context
logEvent("script step=associate_null");
g.g.himcSavedhimcSaved = ImmAssociateContext(hwnd, null);
logEvent("imm associate_context new=0 old=0x%zx now=0x%zx",
cast((unresolved type) size_tsize_t) g.g.himcSavedhimcSaved, cast((unresolved type) size_tsize_t) ImmGetContext(hwnd));
injectScans([0x2e, 0x2e | 0x8000]); // 'c' still arrives: plain WM_CHAR
break;
case 6: // restore
logEvent("script step=associate_restore");
ImmAssociateContext(hwnd, g.g.himcSavedhimcSaved);
logEvent("imm associate_context new=0x%zx now=0x%zx",
cast((unresolved type) size_tsize_t) g.g.himcSavedhimcSaved, cast((unresolved type) size_tsize_t) ImmGetContext(hwnd));
break;
default:
KillTimer(hwnd, TIMER_ID);
char[256] _error_ u8u8;
utf8z(g.g.texttext[0 .. g.len], u8);
logEvent("summary text=%s caret=%d start=%u comp=%u end=%u setctx=%u notify=%u imechar=%u char=%u commit=%u",
u8.ptr, g.g.caretcaret, g.g.nStartnStart, g.g.nCompnComp, g.g.nEndnEnd, g.g.nSetCtxnSetCtx, g.g.nNotifynNotify,
g.g.nImeCharnImeChar, g.g.nCharnChar, g.g.nCommitnCommit);
if (g.g.autoExitautoExit)
DestroyWindow(hwnd);
else
logEvent("script_done note=window_stays_open_for_real_ime_typing");
}
}
// ---------------------------------------------------------------------------
extern (Windows) LRESULT app.wndProcwndProc(HWND (parameter) HWND hwndhwnd, UINT (parameter) UINT msgmsg, WPARAM wParam, LPARAM lParam) nothrow
{
switch (msg)
{
case WM_IME_SETCONTEXT:
++g.g.nSetCtxnSetCtx;
logEvent("msg name=WM_IME_SETCONTEXT active=%d show=0x%llx",
cast(int) wParam, cast(ulong) lParam);
// We render the composition inline, so suppress the IME's own
// composition-window UI; candidate UI stays (the IME draws the list).
return DefWindowProcW(hwnd, msg, wParam,
lParam & ~cast(LPARAM) ISC_SHOWUICOMPOSITIONWINDOW);
case WM_IME_STARTCOMPOSITION:
++g.g.nStartnStart;
logEvent("msg name=WM_IME_STARTCOMPOSITION");
g.g.composingcomposing = true;
anchorCandidateWindow();
return 0; // not DefWindowProc: we own the composition rendering
case WM_IME_COMPOSITION:
onImeComposition(hwnd, lParam);
return 0;
case WM_IME_ENDCOMPOSITION:
++g.g.nEndnEnd;
logEvent("msg name=WM_IME_ENDCOMPOSITION");
g.g.composingcomposing = false;
g.g.preLenpreLen = g.g.preCursorpreCursor = 0;
InvalidateRect(hwnd, null, TRUE);
return 0;
case WM_IME_NOTIFY:
++g.g.nNotifynNotify;
logEvent("msg name=WM_IME_NOTIFY cmd=0x%llx", cast(ulong) wParam);
goto default; // IMN_* housekeeping belongs to DefWindowProc
case WM_IME_CHAR: // result chars arrive here only if GCS_RESULTSTR is
++g.g.nImeCharnImeChar; // left unhandled — counting it proves we consumed it
logEvent("msg name=WM_IME_CHAR utf16=0x%04llx", cast(ulong) wParam);
return 0;
case WM_CHAR:
++g.g.nCharnChar;
const wchar _error_ uu = cast(wchar) wParam;
logEvent("char utf16=0x%04x", u);
if (u == 8 && g.g.caretcaret > 0) // backspace
{
foreach ((parameter) ii; g.g.caretcaret .. g.g.lenlen)
g.g.texttext[i - 1] = g.g.texttext[i];
g.g.lenlen--;
moveCaret(-1);
}
else if (u >= 0x20)
{
wchar[1] _error_ oneone = u;
insertText(one[]);
}
return 0;
case WM_KEYDOWN:
if (wParam == VK_LEFT)
moveCaret(-1);
else if (wParam == VK_RIGHT)
moveCaret(1);
return 0;
case WM_SETFOCUS:
g.g.focusedfocused = true;
CreateCaret(hwnd, null, 1, g.g.lineHeightlineHeight);
ShowCaret(hwnd);
InvalidateRect(hwnd, null, TRUE);
return 0;
case WM_KILLFOCUS:
g.g.focusedfocused = false;
DestroyCaret();
return 0;
case WM_PAINT:
paint(hwnd);
return 0;
case WM_TIMER:
if (wParam == TIMER_ID)
runStep(hwnd);
return 0;
case WM_CLOSE:
logEvent("close_requested");
goto default;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// ---------------------------------------------------------------------------
bool bool app.envFlag(const(wchar)* name) nothrowenvFlag(const(wchar)* (parameter) const(wchar)* namename) nothrow
{
WCHAR[8] (local variable) _error_ bufbuf;
const (local variable) const(_error_) nn = GetEnvironmentVariableW(name, buf.ptr, buf.buf.lengthlength);
return n >= 1 && n < buf.(field) _error_ buf.lengthlength && buf[0] == '1';
}
int int D main()main()
{
instrumentInit("f07_text_input_win32");
logEvent("init_start");
g.g.autoExitautoExit = envFlag("WSI_AUTO_EXIT"w.ptr);
logEvent("mode auto_exit=%d", g.g.autoExitautoExit ? 1 : 0);
HINSTANCE (local variable) _error_ hInsthInst = GetModuleHandleW(null);
auto (local variable) wstring clsNameclsName = "wsi-f07-class"w;
WNDCLASSEXW (local variable) _error_ wcwc;
wc.cbSize = WNDCLASSEXW.sizeof;
wc.lpfnWndProc = &wndProc;
wc.hInstance = hInst;
wc.lpszClassName = clsName.ptr;
wc.hCursor = LoadCursorW(null, IDC_ARROW);
if (!RegisterClassExW(&wc))
{
logEvent("error what=RegisterClassExW code=%lu", GetLastError());
return 1;
}
g.g.hwndhwnd = CreateWindowExW(0, clsName.ptr, "wsi-f07-text-input"w.ptr,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 480, 160,
null, null, hInst, null);
if (g.(field) _error_ g.hwndhwnd is null)
{
logEvent("error what=CreateWindowExW code=%lu", GetLastError());
return 1;
}
logEvent("window_created");
// The decision record: TSF first (and torn down), IMM32 as the editor.
tsfProbe(g.g.hwndhwnd);
ShowWindow(g.g.hwndhwnd, SW_SHOW);
UpdateWindow(g.g.hwndhwnd);
SetForegroundWindow(g.g.hwndhwnd);
SetFocus(g.g.hwndhwnd);
void app.anchorCandidateWindow() nothrowanchorCandidateWindow();
SetTimer(g.g.hwndhwnd, TIMER_ID, TICK_MS, null);
MSG (local variable) _error_ msgmsg;
while (GetMessageW(&msg, null, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
CoUninitialize();
logEvent("exit code=%d", cast(int) msg.wParam);
return cast(int) msg.wParam;
}