IMDUI: an immediate-mode C++ UI framework for Windows
Cheat menus are a strange UI problem. The window has to stay small and readable while someone opens it mid-match. An AI aimbot needs toggles, sliders, keybinds, a color picker for the overlay, and a way to mark which part of the screen to watch. Every control has to work with one hand on a mouse that is also moving a camera.
Xenial, who is a fantastic developer I had the pleasure of working with, and I were building an AI aimbot and a few related cheat products when we hit this. The default answer for a menu like that was ImGui. We used it, and I still think it is a good library. But in a game process, a menu is not only pixels. Every operating system call it makes is something an anti-cheat can watch, and the list is long.
ImGui asks Windows for a lot
Anti-cheat watches what a process calls. A module that polls the foreground window, the cursor position, and raw keyboard state is worth a second look, and a menu library that does it on every frame adds that risk to whatever it is attached to.
Dear ImGui is built to run on many platforms, so its Win32 backend owns the parts of the OS that a platform-agnostic UI needs. ImGui_ImplWin32_NewFrame reads the window rectangle and the performance counter on every frame, then pulls mouse state from the foreground window:
// ImGui_ImplWin32_NewFrame, every frame
RECT rect = { 0, 0, 0, 0 };
::GetClientRect(bd->hWnd, &rect);
io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
...
HWND focused_window = ::GetForegroundWindow();
...
if (::GetCursorPos(&pos) && ::ScreenToClient(bd->hWnd, &pos))
io.AddMousePosEvent((float)pos.x, (float)pos.y);
That is one function. Across the backend, I count 36 distinct Win32 functions: per-frame queries such as GetClientRect and QueryPerformanceCounter; mouse and cursor handling with GetForegroundWindow, GetCursorPos, SetCursorPos, SetCursor, GetCapture, SetCapture, and ReleaseCapture; keyboard translation with GetKeyboardLayout, GetLocaleInfoA, MultiByteToWideChar, IsDBCSLeadByte, and IsWindowUnicode; startup work such as SetProcessDPIAware, DwmGetColorizationColor, DwmEnableBlurBehindWindow, GetDC, GetDeviceCaps, and MonitorFromWindow; and a version check that loads ntdll.dll with GetModuleHandleA to call RtlVerifyVersionInfo. The core library carries its own Windows code too, for the clipboard (OpenClipboard, GetClipboardData, SetClipboardData, GlobalLock) and for IME text input (ImmGetContext, ImmSetCompositionWindow).
None of those calls is wrong in isolation. Together they are a fingerprint. Most processes do not ask the DWM for its colorization settings, and an external module that does looks different from the game around it. ImGui is the common choice for overlays like this one, so the pattern is already familiar to the tools that look for it.
This is not a flaw in ImGui. It is built to be the default UI on every platform and every kind of application, so it has to ask the system for everything. On the desktop that work is invisible. In an injected menu, all of that OS access is risk we did not want to ship.
What we built instead
IMDUI works from a smaller contract with Windows. The host already owns a window procedure, so it forwards its messages to processWindowMessage, and the library keeps input state as those messages arrive. It does not poll the OS on a schedule.
if (auto panel = imdui::drawRectangle(24, 24, 320, 180)
.asColumn(12)
.color(imdui::Color::fromHex(0x18222D))
.cornerRadius(10)
.enter())
{
imdui::drawText(L"Preferences").fontSize(20);
imdui::drawText(L"Enable overlay").widthExpand();
imdui::drawRectangle(0, 0, 40, 20)
.color(imdui::Color::fromHex(0x28587A))
.onClick(toggleOverlay, &state);
}
The panel knows it is a column with a 12 pixel gap. Size, color, and corners are properties of the node, set with methods that return the same node, so the description reads top to bottom. Nesting uses an RAII guard instead of a begin/end pair, and the compiler closes the parent even if the function returns early.
The Win32 surface of the library is about a dozen functions: GetClientRect once at startup, GetModuleFileNameA and MultiByteToWideChar for file paths, MapVirtualKeyW and GetKeyNameTextW for key labels, GetCapture, SetCapture, and ReleaseCapture for drag capture, GetFocus and SetFocus, GetKeyState only when the application asks whether a key is down, ScreenToClient for the wheel position, and TrackMouseEvent for mouse leave. There is no clipboard, no IME, no DWM, no GDI, no cursor management, and no DPI query. Rendering is Direct3D 11 and nothing else.
Under the hood
IMDUI is immediate mode in the same sense ImGui is. The application declares the whole tree between newFrame() and endFrame(), and nothing persists except animation state and widget values. The layout solver runs at endFrame() and resolves each node from pixels, a percentage of the parent, fit to content, a ratio of the other axis, or a weighted expansion into unused space. Auto is the default when none of those apply. Min and max clamps sit on top. Positions can anchor to another node with pixel or percentage offsets, which is how the menu centers controls without magic numbers.
The solver has one rule it will not guess around. A node needs an independent dimension before it can derive a dependent one. A content-sized parent full of percentage-sized children has no answer, and the solver refuses to invent one. That refusal showed up early, and it is why the sizing modes have to compose in a straight line.
Node storage is a pair of arenas that alternate every frame. Declaring a node stops allocating once the arenas have grown enough, and NodeRef handles are only valid for a frame or two. That sounds restrictive, and it is. It is also why the frame loop does not slowly turn into a leak. Anything that needs to survive lives in an application value or in the widget that wraps it, not in the layout tree.
The renderer batches the tree into Direct3D 11 draw calls. Text uses an embedded Inter atlas, SVG images rasterize into the same batches, clipping becomes scissor rectangles, and overlay nodes draw after the normal pass so a popup can escape its panel. The library owns one HWND, one device, and one swap chain, and every UI call belongs on the window’s thread. There is no backend abstraction to write and no renderer to choose.
Input arrives as window messages. The host forwards WM_KEYDOWN, WM_MOUSEMOVE, WM_CHAR, and the rest to processWindowMessage, and the adapter keeps key state, mouse position, wheel deltas, and text events for the current frame. It also reports a click when the press and release both land between frames. Menus eat those fast clicks otherwise, and a menu that misses input is worse than a menu with fewer features.
The parts a cheat menu needed
The first real user of IMDUI was our own settings menu, which became the Menu class. It draws sections and tabs, and each row is a small control with its own behavior. Toggles and checkboxes bind to a bool. Sliders bind to a float range. Dropdowns can search. The color picker edits RGBA. A keybind row captures a Win32 virtual key, with an optional second key for combinations. Rows take tooltip text, and a hidden flag removes one from view without deleting it from the array.
The menu docks to any edge of the window and can collapse itself after a period of inactivity. Both exist because a cheat menu that covers the game is a menu you close instead of use.
The control that came straight from the aimbot is the region selector. An aimbot has to know which part of the screen to look at, and asking users to type coordinates is a bad joke. requestRegionSelection puts the app into a mode where the user drags out labeled rectangles, and the coordinates land in application-owned structs. That control has nothing to do with settings, but it stayed in the library because any app that captures or crops a window needs the same thing. It is the clearest example of a cheat requirement turning into a general feature, which is most of why the library was worth releasing.
Open source
IMDUI is on GitHub under the MIT license. It builds with CMake 3.25 or newer and needs a C++23 compiler, Windows 10, and the Windows SDK. The build produces a static library, a desktop example, and integration tests that run against a hidden Win32 window and a real D3D11 device, with WARP as a fallback. The Inter atlas is embedded, so there are no asset files to ship. The repo includes documentation for the core API, the layout engine, widgets, animation, and the Direct3D resources.
I work on anti-cheat now, so I do not build cheat menus anymore. The framework outlived the products it was written for, which is a better ending than a settings menu usually gets.