lstwoMODS Core • Custom UI

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 poke Data directly, call MarkChanged() 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.

MethodEffect
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):

MethodEffect
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

ElementRenders
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
ElementNotes
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)
ElementValue typeNotes
Checkbox(name, value, onChanged, mainThread)boolWithValue(Ref<bool>).
RadioButton(name, label, Ref<int> group, optionValue, onChanged, mainThread)intButtons sharing a group ref form one selection.
Selectable(name, selected, onChanged, flags, sizeX, sizeY, mainThread)boolWithSelected(Ref<bool>).
Combo(name, items, selectedIndex, onChanged, flags, mainThread)int indexWithSelectedIndex(Ref<int>), WithItems(Ref<string[]>).
SearchableCombo(...)int indexSame API as Combo plus a filter box in the dropdown.
ProgressBar(name, value, sizeX, sizeY, overlay)floatDisplay 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")
MethodEffect
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).

MemberEffect
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.
IsFocusedCurrent 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:

FamilyParameters after name
Drag*value, speed, min, max, format, onValueChanged, flags, mainThread
Slider*value, min, max, format, onValueChanged, flags, mainThread
SliderAngleangleRad, 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

ComponentsFloat typeInt type
2Vec2Vec2Int
3Vec3Vec3Int
4Vec4Vec4Int

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:

ElementCall
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.

MemberEffect
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:

MemberEffect
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")));
MemberEffect
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));
MemberEffect
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"))
ElementNotes
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).

SeriesConstructor
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

ElementPurpose
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)
ComponentNotes
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.