UI Elements
Every piece of UI in lstwoMODS is a BaseUIElement. You build a tree of them, hand the root
to a window (or return it from BuildPanel / ConstructUI), and the overlay process renders
the actual ImGui frame from it.
All elements live in lstwoMODS_Core.UI.Elements.
How elements work
The mod side never calls ImGui directly. Each element owns a Data object that gets sent to
the overlay, and the overlay sends state back (what the user typed, clicked, dragged), which
fires your callbacks. Two consequences worth knowing up front:
- Callbacks arrive from the IPC thread. By default they are marshalled onto Unity’s main thread for you. See the Threading section at the end of this page.
- Changes need
MarkChanged(). Setting a property through the element’s own accessors (myText.Text = "hi") does this automatically. If you pokeDatadirectly, callMarkChanged()yourself or the overlay never hears about it.
Names and labels
Every constructor takes a name as its first parameter. It is the element’s unique ID, and for
most widgets it doubles as the visible ImGui label:
new Button("Apply") // shows "Apply"
new Button("Apply##save-btn") // shows "Apply", ID is "Apply##save-btn"
new Button("###save-btn") // shows nothing, ID is stable Use the ImGui ## convention when two elements would otherwise share a label. Some elements
(CollapsingHeader, TreeNode, RadioButton, Modal, GuiWindow, TabItem, Menu) take a
separate label parameter, so the name stays a pure ID there.
Binding values with Ref<T>
Ref<T> is the two-way binding primitive. Assigning Value pushes into the UI, and user edits
push back into the ref:
public static Ref<float> Speed = new(1f);
new DragFloat("Speed").WithValue(Speed) Every input element has a WithValue(...) (or WithSelectedIndex, WithSelected, WithAngleRad) overload. Ref<T>.Changed fires on every set, including sets to the same
value. Prefer refs over the onValueChanged constructor parameter when the value can also
change from game code, because a plain callback never updates what the widget displays.
Common API
Every element inherits these from BaseUIElement<TSelf>. They are all chainable and return the
concrete element type, so no casts are needed.
| Method | Effect |
|---|---|
WithTooltip(text, hoveredFlags) | Hover tooltip. Also accepts a Ref<string>. |
WithFont(name) | Push a font registered via Window.AddFont. null = default font. |
WithStyleVar(var, value) / WithStyleVar(var, x, y) | Push a float or Vec2 ImGuiStyleVar. |
WithStyleColor(col, r, g, b, a) | Push an ImGuiCol override. |
WithStyleColorAlpha(col, alpha) | Keep the theme’s RGB for col, override only alpha. Resolved live, so theme changes track. |
WithId(id) | Push a string onto the ImGui ID stack. |
WithPreset(preset) | Apply a StylePreset (see Styling). |
WithItemWidth(width) | Width of child widgets. Positive = px, 0 = default, -1 = fill. Also accepts a Ref<float>. |
WithItemFlags(flags, enable) | Apply ImGuiItemFlags to children. |
WithDisabled(bool) | Gray out and block interaction. Also accepts a Ref<bool>. |
WithVisible(Ref<bool>) | Show or hide, driven by a ref. |
WithTextWrapPos(x) | Text wrap position. 0 = window right edge, negative = off. |
WithClipRect(minX, minY, maxX, maxY, intersect) | Clip children to a screen rect. |
WithTabStop(bool) | Whether children take part in tab navigation. |
WithRequireInput(bool / RequireInputMode / Ref<bool>) | Whether this subtree needs mouse and keyboard focus. |
RunOnMainThread(Ref<bool>) | Toggle main-thread callback dispatch. |
And these methods (not chainable):
| Method | Effect |
|---|---|
SetVisible(bool) | Show or hide this element and its children at runtime. |
SetDisabled(bool) | Toggle disabled state at runtime. |
MarkChanged() | Flag the element dirty so its data is sent next frame. |
GetChildren() | Enumerate child elements. |
Properties: Name, Data, Parent, IsTopLevel, CanHoldChildren, RunCallbacksOnMainThread.
Text
| Element | Renders |
|---|---|
UIText(name, text) | ImGui.Text, a plain label. |
TextColored(name, text, Col) | Colored text. Also takes (r, g, b, a). |
TextDisabled(name, text) | Dimmed text. |
TextWrapped(name, text) | Wraps at the window edge. |
LabelText(name, label, text) | Label left, value right. |
BulletText(name, text) | Bullet point. |
SeparatorText(name, label) | Separator line with a centered label. |
The class is UIText, not Text, to avoid the clash with ImGui’s own naming. All of them take WithText(Ref<string>); LabelText also has WithLabel(Ref<string>).
new UIText("status", "Idle").WithText(StatusRef)
new TextColored("warn", "Host only", 1f, 0.6f, 0.2f) Buttons
new Button("Apply", () => Apply())
new Button("Apply", () => Apply()).WithContentWidth() // match CalcItemWidth, aligns with inputs | Element | Notes |
|---|---|
Button(name, onPressed, mainThread) | Standard button. WithContentWidth(bool / Ref<bool>) matches input widget width. |
SmallButton(name, onPressed, mainThread) | Compact button, no frame padding. |
ArrowButton(name, ImGuiDir, onPressed, mainThread) | Directional arrow. |
InvisibleButton(name, sizeX, sizeY, onPressed, mainThread) | Invisible clickable area. |
UIImageButton(name, filePath, w, h, onPressed, mainThread) | See Images. |
All expose an OnPressed event you can subscribe to after construction.
Toggles and selection
new Checkbox("Enabled").WithValue(EnabledRef)
var quality = new Ref<int>(1);
new RadioButton("q-low", "Low", quality, 0)
new RadioButton("q-high", "High", quality, 1)
new Combo("Mode", new[] { "Off", "Auto", "Manual" }).WithSelectedIndex(ModeRef) | Element | Value type | Notes |
|---|---|---|
Checkbox(name, value, onChanged, mainThread) | bool | WithValue(Ref<bool>). |
RadioButton(name, label, Ref<int> group, optionValue, onChanged, mainThread) | int | Buttons sharing a group ref form one selection. |
Selectable(name, selected, onChanged, flags, sizeX, sizeY, mainThread) | bool | WithSelected(Ref<bool>). |
Combo(name, items, selectedIndex, onChanged, flags, mainThread) | int index | WithSelectedIndex(Ref<int>), WithItems(Ref<string[]>). |
SearchableCombo(...) | int index | Same API as Combo plus a filter box in the dropdown. |
ProgressBar(name, value, sizeX, sizeY, overlay) | float | Display only. WithValue(Ref<float>), WithOverlay(Ref<string>). |
KeyCapture
A button that records a key combination. By default the user clicks it to start listening, it
shows “Press a key…”, and the first combination fires OnCaptured(ImGuiKey, HotkeyModifiers).
new KeyCapture("bind", (key, mods) => Save(key, mods))
.WithInlineLabel("Toggle key") | Method | Effect |
|---|---|
WithAlwaysListen(bool) | Capture continuously, no click needed. |
WithListeningText(text) | Label while listening. Default “Press a key…“. |
WithContentWidth(bool) | Match CalcItemWidth instead of stretching. |
WithLabel(text) | Text to the right of the button, where ImGui puts input labels. |
WithInlineLabel(text) | Shorthand for WithContentWidth().WithLabel(text). Lays out like a normal setting row. |
WithDisplay(idleText) | Idle label, usually the current binding. |
Reset(idleText) | Start a fresh session. Call when your capture UI opens. |
Stop() | Stop listening, keep the idle label. |
Text input
new InputText("Message", hint: "Say something...")
.WithValue(MessageRef)
.WatchKeys(ImGuiKey.Enter, ImGuiKey.Tab, ImGuiKey.UpArrow)
.OnKey(HandleKey) Constructor: InputText(name, value, hint, maxLength, multiline, sizeX, sizeY, flags, onChanged, mainThread).
| Member | Effect |
|---|---|
WithValue(Ref<string>) | Two-way bind the text. |
WatchKeys(params ImGuiKey[]) | Keys the renderer reports back while the input is focused. |
OnKey(Action<ImGuiKey>) | Fires for any watched key. |
OnFocus(Action<bool>) / WithFocus(Ref<bool>) | Focus gained or lost. |
FocusNextFrame() | Ask the renderer to grab keyboard focus. |
IsFocused | Current focus state. |
Watch ImGuiKey.Enter explicitly if a single reliable Enter press matters. The EnterReturnsTrue path is gated on the input having been active the previous frame.
Numeric input
Three families, each in scalar, 2, 3 and 4 component forms:
- Drag: click and drag to change.
DragFloat,DragFloat2/3/4,DragInt,DragInt2/3/4. - Slider: bounded track.
SliderFloat,SliderFloat2/3/4,SliderInt,SliderInt2/3/4,SliderAngle. - Input: typed box with optional step buttons.
InputFloat,InputFloat2/3/4,InputInt,InputInt2/3/4.
new DragFloat("Speed", value: 1f, speed: 0.1f, min: 0f, max: 10f).WithValue(SpeedRef)
new SliderInt("Count", value: 5, min: 0, max: 20).WithValue(CountRef)
new InputFloat("Offset", step: 0.1f, stepFast: 1f).WithValue(OffsetRef)
new SliderAngle("Rotation").WithAngleRad(AngleRef) // stored in radians, shown in degrees Constructor shapes:
| Family | Parameters after name |
|---|---|
Drag* | value, speed, min, max, format, onValueChanged, flags, mainThread |
Slider* | value, min, max, format, onValueChanged, flags, mainThread |
SliderAngle | angleRad, minDegrees, maxDegrees, format, onValueChanged, flags, mainThread |
InputFloat* | value, step, stepFast, format, onValueChanged, flags, mainThread |
InputInt* | value, step, stepFast, onValueChanged, flags, mainThread |
min == max == 0 on a drag means unbounded. Default formats are "%.3f" for floats and "%d" for ints.
Multi-component value types
| Components | Float type | Int type |
|---|---|---|
| 2 | Vec2 | Vec2Int |
| 3 | Vec3 | Vec3Int |
| 4 | Vec4 | Vec4Int |
These live in lstwoMODS_Core and convert implicitly to and from Unity’s Vector2/3/4 and Vector2Int/3Int. Each element has both a Vec-typed and a Unity-typed WithValue overload,
for example WithValue(Ref<Vec3>) and WithValue(Ref<Vector3>).
Prefer the Vec types for anything that gets saved: Unity’s Vector and Color types do not
survive a JSON round trip (their derived properties recurse). There is no Vector4Int in Unity,
so the 4-component int elements take Vec4Int or an (int X, int Y, int Z, int W) tuple.
The 4-component int elements also take their initial value as loose x, y, z, w parameters
rather than a struct.
Color
new ColorEdit4("Tint").WithValue(TintRef) // Ref<Col> or Ref<Color> ColorEdit3(name, value, onChanged, flags, mainThread) edits RGB, ColorEdit4 edits RGBA. Both
use Col, a serializable RGBA struct that converts implicitly to and from UnityEngine.Color, System.Drawing.Color and the numeric vector types. Channels are readable as both R/G/B/A and lowercase r/g/b/a.
Persist Col, never UnityEngine.Color.
Layout and spacing
These map one to one onto ImGui layout calls and hold no state:
| Element | Call |
|---|---|
Separator(name) | ImGui.Separator() |
Spacing(name) | ImGui.Spacing() |
NewLine(name) | ImGui.NewLine() |
SameLine(name, offsetX, spacing) | ImGui.SameLine() |
Dummy(name, sizeX, sizeY) | Invisible placeholder item. |
Indent(name, amount, unindent) | ImGui.Indent() / Unindent(). |
AlignText(name) | AlignTextToFramePadding(), centers text next to taller widgets. |
SetCursorPos(name, x, y, screenSpace) | Manual positioning. |
SetNextItemWidth(name, width) | Width of the next widget only. |
Columns(name, count, borders, id) / NextColumn(name) | Legacy column layout. |
FocusNext(name, offset) | SetKeyboardFocusHere. 0 = next widget, -1 = previous. |
FocusDefault(name) | SetItemDefaultFocus, e.g. inside a combo dropdown. |
NextItemShortcut(name, key, mod1, mod2, flags) | Keyboard shortcut for the next widget. |
Elements render in tree order, one per line, unless you insert a SameLine between them.
Containers
Group and Container
new Group("settings",
new UIText("hdr", "Settings"),
new Checkbox("Enabled")) Group wraps children in BeginGroup/EndGroup, so they act as one item for layout and
hovering. It also has SetChildOrder(orderedChildren) for reordering existing children at
runtime (children you leave out keep their relative order at the end).
Container is transparent: no ImGui wrapper at all, children keep independent layout positions.
This is what BuildPanel returns. Setting SetVisible(false) on either hides the whole subtree.
ChildWindow
A scrollable, optionally bordered sub-region.
new ChildWindow("log", 0f, 0f, logLines)
.WithFooterReserve(1f) // leave room for a button row below Constructor: ChildWindow(name, sizeX, sizeY, children), where 0 on either axis fills the
remaining space.
| Member | Effect |
|---|---|
WithFlags(ImGuiChildFlags) / WithWindowFlags(ImGuiWindowFlags) | ImGui flags. |
WithFooterReserve(lines) | Fill remaining height minus room for N widget rows, so following siblings pin to the bottom. Overrides sizeY. |
ScrollToBottom() / ScrollToTop() / ScrollTo(ratio) | Scroll on the next frame. |
SetScrollY(px) | Absolute vertical scroll. |
CollapsingHeader and TreeNode
Both take (name, label, children) and support DefaultOpen(), WithFlags(ImGuiTreeNodeFlags), OnToggle(Action<bool>), and drag and drop on their header line.
new CollapsingHeader("adv", "Advanced", body).DefaultOpen()
new TreeNode("node-1", "Group 1", body)
.WithLineTag("3 items")
.WithLineElements(new SmallButton("X##del-1", Delete))
.PinLineElementsEnd() CollapsingHeader extras: WithClose() adds an X button that fires OnToggled(false),
and Label is settable at runtime, or bindable with WithLabel(Ref<string>).
TreeNode extras:
| Member | Effect |
|---|---|
WithLineElements(params elements) | Put widgets on the node’s own line. They stay visible while collapsed. |
PinLineElementsEnd() | Right-align those line elements. |
WithLineTag(tag, tooltip) | Dimmed text between label and line elements, auto-ellipsized to fit. |
Drag and drop on both:
header.WithDragSource("my-payload", itemId, "Item 1")
.WithDropTarget((type, data, below) => Reorder(data, below), "my-payload"); WithDragSource(payloadType, payloadData, displayLabel) makes the bar draggable while
click-to-expand keeps working. WithDropTarget(onDrop, acceptTypes) gives insert-between
semantics: the overlay draws an insertion line above or below the bar depending on the mouse
half, and onDrop receives (payloadType, payloadData, droppedBelow).
HStack
Side by side children with proportional slot widths. Input widgets and buttons inside each slot auto-size to fill it.
new HStack("row", sliderA, sliderB, sliderC) // equal thirds, full width
new HStack("row", label, input, btn).WithProportions(1f, 2f, 1f) // 1 : 2 : 1
new HStack("row", a, b).WithContentWidth() // natural sizes
new HStack("row", a, b).WithWidth(300f) // fixed total
new HStack("row", a, b).WithSpacing(8f) // custom gap, -1 = ItemSpacing.X FlowGrid
Responsive grid that fits as many columns as the width allows and stretches cells to fill each row edge to edge.
new FlowGrid("cards", minCellWidth: 240f, maxCellWidth: 0f, cards)
.WithTail(dropZone) maxCellWidth: 0 means stretch without limit. WithTail(element) stretches an element into the
trailing empty space of the last partial row, useful as an “after the last cell” drop zone. ChildWindow children adopt the cell width automatically.
PinRow
A one-line row where lead content flows from the left and trailing elements pin to the right edge. When the trailing elements do not fit, they wrap onto their own right-aligned line instead of overflowing.
new PinRow("hdr", new UIText("t", "Section title"))
.WithTrailing(new SmallButton("Edit"), new SmallButton("Delete")) Lead content follows the Container convention: insert explicit SameLine elements between
widgets that should share a line. Trailing elements are laid out side by side automatically.
Table
new Table("stats", flags: ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)
.WithColumn("Name", 120f)
.WithColumn("Value")
.WithHeadersRow()
.WithRows(
new TableRow(new UIText("n1", "Speed"), new UIText("v1", "12.5")),
new TableRow(new UIText("n2", "Health"), new UIText("v2", "100"))); | Member | Effect |
|---|---|
WithColumn(label, width, colFlags) | One call per column, in order. |
WithRows(params TableRow[]) / AddRow(row) | Add rows. |
WithHeadersRow() | Render the header row. |
WithSize(sizeX, sizeY) / WithInnerWidth(w) | Sizing. |
WithScrollFreeze(cols, rows) | Freeze leading columns and rows while scrolling. |
WithFlags(ImGuiTableFlags) | Sorting, resizing, borders, and so on. |
Each TableRow’s children map one to one onto columns, left to right. Wrap a cell in a Group or Container for more than one widget. TableRow supports WithHeight(h) and WithFlags(ImGuiTableRowFlags), and generates its own name.
TabBar
new TabBar("tabs",
new TabItem("t-general", "General", generalBody),
new TabItem("t-advanced", "Advanced", advancedBody).WithClose(OnTabClosed)); TabBar takes WithFlags(ImGuiTabBarFlags). TabItem takes WithFlags(ImGuiTabItemFlags) and WithClose(onClose, mainThread) for an X button.
Windows, popups and menus
GuiWindow
A floating ImGui window inside the overlay. Note this is different from BaseWindow, which
registers a panel in the lstwoMODS overlay itself (see the Creating Windows guide).
new GuiWindow("my-win", "My Window", body)
.WithSize(400f, 300f)
.Centered()
.WithFlags(ImGuiWindowFlags.NoCollapse)
.OnOpen(open => Log(open)); | Member | Effect |
|---|---|
WithSize(w, h, ImGuiCond) | Size, applied via SetNextWindowSize. Default Once. |
WithPosition(x, y, cond, pivotX, pivotY) | Position. Pivot 0 = top-left aligned, 0.5 = centered, 1 = bottom-right. |
Centered(cond) | Center on the display. |
CenteredX(y, cond) | Horizontally centered at a fixed Y. Defaults to Always. |
WithTitle(Ref<string>) | Bind the title. |
WithFlags(ImGuiWindowFlags) | Window flags. |
WithNoClose() | Hide the X button. |
WithOpen(bool) / WithOpen(Ref<bool>) | Initial or bound open state. |
WithContentSize(x, y) | Inner scrollable content size, e.g. to force a horizontal scrollbar. |
WithDock(dockSpaceId, cond) | Dock into a DockSpace on first use. |
PinToMainViewport(bool) | Keep on the primary display; positions become viewport-relative offsets. |
FocusNextFrame() | Bring to front. |
OnOpen(Action<bool>, mainThread) | Open state changed, including the first frame shown. |
OnFocus(Action, mainThread) | Focus gained. Suppressed on the just-opened frame. |
Modal and Popup
var modal = new Modal("confirm", "Delete?",
new UIText("msg", "This cannot be undone."),
new Button("OK", () => { modal.Close(); Delete(); }))
.WithSize(320f, 140f)
.WithNoClose();
modal.Open(); Both have Open(), Close(), IsOpen, WithFlags(ImGuiWindowFlags) and OnClose(Action). Modal adds SetTitle(title) (set it before opening, retitling an open modal resets its
position) and WithSize(w, h), which is required when content stretches to fill the modal since
auto-fit cannot measure stretchy children. WithNoClose() hides the X.
For simple confirm and alert flows, use the prebuilt ConfirmDialog and AlertDialog under Prebuilt components instead.
Context menus and menu bars
new ContextMenu("row-menu", rowContent,
new MenuItem("Rename", Rename),
new MenuItem("Delete", Delete).WithShortcut("Del")) | Element | Notes |
|---|---|
ContextMenu(name, trigger, items) | Renders trigger, opens items on right-click. OnWindow() switches from “right-click the previous item” to “right-click anywhere in the window”. WithFlags(ImGuiPopupFlags). |
MenuItem(name, onClicked, mainThread) | WithShortcut(text), WithSelected(bool) (adds a checkmark), WithItemEnabled(bool), OnClick(cb). |
Menu(name, label, children) | Submenu. WithEnabled(bool). |
MenuBar(name, children) | Menu bar in the parent window. Needs ImGuiWindowFlags.MenuBar on the window. |
MainMenuBar(name, children) | Full-width bar fixed at the top of the display. |
ClosePopup(name) | Calls CloseCurrentPopup(). Place inside a popup or modal. |
ModContextMenu.ForSetting / ForAction / Items wrap a widget with the standard
“Add to macro” and “Create hotkey” entries, plus any extra items you pass.
Drag and drop
Beyond the header-line drag and drop on CollapsingHeader and TreeNode, two wrapper elements
make arbitrary content draggable:
new DragSource("src-1", "my-payload", itemId, content)
.WithDisplayLabel("Item 1")
new DragTarget("dst-1", (type, data) => Accept(data), new[] { "my-payload" }, content) DragSource(name, payloadType, payloadData, children) makes the children’s bounding box
draggable. DragTarget(name, onDrop, acceptTypes, children) makes theirs a drop zone. WithInsertBetween(onDropBetween) switches the target to insert-between mode: the overlay draws
a vertical insertion line at the left or right edge by cursor half and the callback receives (payloadType, payloadData, after).
Payload type strings must match between source and target.
Images
new UIImage("logo", "Assets/logo.png", 128f, 128f)
.WithUV(0f, 0f, 0.5f, 1f)
.WithTint(1f, 1f, 1f, 0.8f)
new UIImageButton("btn", "Assets/icon.png", 24f, 24f, OnClick)
.WithBackground(bgCol) Paths are relative to the overlay’s working directory and loaded on the overlay side. Call window.PreloadImage(path) to load a texture before the first render that needs it.
For icons, prefer Lucide glyphs (see Styling): they render inline in any label with no image loading at all.
Plots
Simple ImGui plots
PlotLines(name, values, overlayText, scaleMin, scaleMax, sizeX, sizeY) and PlotHistogram(...) with the same signature. Assign Values to update; the setter marks the
element changed for you.
ImPlot
PlotPanel is the container, series are its children.
new PlotPanel("fps-plot", "Frame time", -1f, 220f,
new PlotLineSeries("ms", xs, ys))
.WithXAxis("t")
.WithYAxis("ms")
.WithLimits(0, 100, 0, 33)
.WithLegend(ImPlotLocation.NorthEast); PlotPanel methods: WithFlags(ImPlotFlags), WithXAxis(label, flags), WithYAxis(label, flags), WithLimits(xMin, xMax, yMin, yMax, cond), WithLegend(location, flags).
| Series | Constructor |
|---|---|
PlotLineSeries | (name, xs, ys, ImPlotLineFlags) |
PlotScatterSeries | (name, xs, ys, ImPlotScatterFlags) |
PlotBarSeries | (name, xs, ys, barWidth, ImPlotBarsFlags) |
PlotStairsSeries | (name, xs, ys, ImPlotStairsFlags) |
PlotStemsSeries | (name, xs, ys, baseline, ImPlotStemsFlags) |
PlotShadedSeries | (name, xs, ys1, ys2, ImPlotShadedFlags) |
ImPlotHistogram | (name, values, bins, ImPlotHistogramFlags), plus WithRange(min, max) |
PlotHeatmap | (name, values, rows, cols, scaleMin, scaleMax, labelFmt, flags) |
PlotPieChart | (name, values, labels, x, y, radius, labelFmt, flags) |
PlotAnnotation | (name, x, y, text, clamp, pixOffX, pixOffY), plus WithColor, WithOffset |
PlotDragLine | (name, dragId, value, vertical, color, thickness, flags, onValueChanged, mainThread) |
Most series have an Update(xs, ys) helper that swaps both arrays and marks changed in one call.
ImPlot3D
Plot3DPanel(name, title, sizeX, sizeY, children) with WithFlags(ImPlot3DFlags) and WithAxes(x, y, z, xf, yf, zf). Children: Plot3DLineSeries, Plot3DScatterSeries (both (name, xs, ys, zs, flags)) and Plot3DSurface(name, xs, ys, zs, rows, cols, flags). All three
have Update(xs, ys, zs).
Node editor
var editor = new NodeEditor("graph",
new GraphNode(1, "Input",
new OutputPin(11, ImNodesPinShape.CircleFilled, new UIText("o1", "out"))),
new GraphNode(2, "Output",
new InputPin(21, ImNodesPinShape.CircleFilled, new UIText("i1", "in"))))
.WithMiniMap()
.OnLinkCreate((start, end) => editor.AddLink(nextId++, start, end))
.OnLinkDestroy(id => editor.RemoveLink(id)); NodeEditor holds GraphNode children and a list of links you manage yourself: the callbacks
tell you what the user did, and AddLink(id, startAttrId, endAttrId) / RemoveLink(id) commit
it. WithMiniMap(location) enables the minimap.
GraphNode(nodeId, title, children) supports WithPosition(x, y) and WithNoTitleBar().
Pins are InputPin(attributeId, shape, children), OutputPin(...) and StaticPin(attributeId, children) for non-connectable content. Node and pin names are derived
from their IDs.
Gizmos
Gizmo(name, view, projection, model, ImGuizmoOperation, ImGuizmoMode, onChanged, mainThread) renders a translate, rotate or scale handle over a 3D scene. Unity Matrix4x4 values are
converted to and from the column-major convention ImGuizmo expects. Call SetCamera(view, projection) each frame, read the edited transform from ModelMatrix or the OnChanged callback, and configure with WithOperation, WithMode and WithSnap.
ViewManipulator(name, view, length, posX, posY, sizeX, sizeY, onChanged, mainThread) is the
orientation cube. SetView(view) pushes a new camera view in, OnChanged reports user rotation.
Dev and debug
| Element | Purpose |
|---|---|
DemoWindow(name) | The ImGui demo window, useful for exploring widgets and styles. |
UIInspector(name) | Inspect the live element tree. |
DockSpace(name, dockSpaceId, ImGuiDockNodeFlags) | Host area for docked GuiWindows. Pair with GuiWindow.WithDock(id). |
Prebuilt components
UIComponent is a Container subclass that bundles elements into a reusable widget. These ship
with core:
new Section("audio", "Audio Settings",
new SliderFloat("Volume", 0.8f, 0f, 1f),
new Checkbox("Mute"))
new OptionGroup("quality", qualityRef,
("Low", 0), ("Medium", 1), ("High", 2))
new ToggleButton("pause", "Pause Game", "Resume Game",
onChanged: paused => Time.timeScale = paused ? 0f : 1f) | Component | Notes |
|---|---|
Section(name, title, content) | SeparatorText header with an indented body. |
OptionGroup(name, Ref<int>, params (label, value)) | Radio buttons on one line sharing a selection. SelectedValue reads it back. |
ToggleButton(name, labelOff, labelOn, initialState, onChanged, mainThread) | Button that swaps labels each press. WithState(Ref<bool>), WithContentWidth(), State. |
ConfirmDialog(name, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, mainThread) | Confirm/cancel modal. Add once, then Show() or Show(message). |
AlertDialog(name, title, message, dismissLabel, onDismissed, mainThread) | One-button alert. Same Show pattern. |
var dlg = new ConfirmDialog("del-dlg", "Delete Item", onConfirm: DeleteItem);
// ...add dlg to your panel, then:
new Button("Delete", () => dlg.Show("Delete this item?\nThis cannot be undone.")) Writing your own
Derive from UIComponent and call Add(...) from the constructor. It renders as a BeginGroup,
so no overlay-side code is ever needed:
public class LabeledSlider : UIComponent
{
private readonly SliderFloat _slider;
public LabeledSlider(string name, string label) : base(name)
{
_slider = new SliderFloat(name + "-s", 0f, 0f, 1f);
Add(new UIText(name + "-lbl", label), _slider);
}
public float Value => _slider.Value;
} Use it anywhere an element is accepted.
Adding and removing elements at runtime
Elements can be added and removed after the window is built, through the window itself:
window.AddElement(newRow, parentGroup); // append into a container
window.AddElement(newRow, parentGroup, 0); // insert at index 0
window.AddElement(floatingWindow); // top level
window.RemoveElement(oldRow); The parent must already be part of that window and must be container-like (Group, Container, ChildWindow, CollapsingHeader, TreeNode, Modal, GuiWindow, and so on). Check with element.CanHoldChildren. AddElement and RemoveElement handle the whole subtree, including
registration and the overlay sync; editing a Children list directly does not, because updated
elements are serialized without their children.
See Editing the Element Tree for HUD windows, dynamic lists and the ordering rules that come with them.
Styling
StylePreset
A reusable bundle of push commands with the same With* surface as elements
(WithFont, WithStyleVar, WithStyleColor, WithStyleColorAlpha, WithId, WithItemWidth, WithItemFlags, WithDisabled, WithTextWrapPos, WithClipRect, WithTabStop, and WithPreset to compose):
static readonly StylePreset Danger = new StylePreset()
.WithStyleColor(ImGuiCol.Button, 0.7f, 0.2f, 0.2f, 1f)
.WithStyleVar(ImGuiStyleVar.FrameRounding, 6f);
new Button("Delete", Delete).WithPreset(Danger) Global style and fonts
On the window: WithGlobalStyleVar, WithGlobalStyleColor, WithGlobalPreset apply to all
ImGui including popups and tooltips. AddFont(name, filePath, size, merge, glyphOffsetY) registers a font you can then push with WithFont(name); it works before or after
initialization, and after it the overlay rebuilds the atlas immediately. RegisteredFontNames lists what is available.
Icons and fonts
Lucide icons are merged into the default font, so any glyph constant renders inline in a label:
new Button($"{Lucide.Save} Save", Save)
new CollapsingHeader("adv", $"{Lucide.Settings} Advanced", body) Constants are on lstwoMODS_Core.UI.Lucide, named after the Lucide icon in PascalCase.
Threading
Every callback is dispatched onto Unity’s main thread by default, so Unity APIs are safe to call
from them. Pass mainThread: false to a constructor (or set RunCallbacksOnMainThread = false)
to fire immediately on the IPC reader thread instead.
Use the IPC thread only for handlers that touch no Unity API and must keep working while the game is frozen or loading, since the main thread is not ticking then.
Ref<T> follows the same rule and has its own RunCallbacksOnMainThread flag plus a chainable WithMainThreadCallbacks(bool). A ref changed on the main thread raises Changed inline with no
frame of latency; one changed from the IPC thread is queued to the next Update. To marshal
manually, use MainThread.Enqueue(...).
Element bindings created with WithValue and friends fire from whichever thread the change came
from, so anything Unity-related inside a Changed handler needs main-thread dispatch.
When none of these fit
If no element covers what you need, you can add your own element type with its own ImGui rendering code. That needs an overlay plugin: see Custom Elements.