Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProwlEngine/Prowl.Paper/llms.txt

Use this file to discover all available pages before exploring further.

Immediate-mode UI (IMGUI) flips the traditional model on its head. Instead of creating widget objects once and then mutating them over time, you describe your entire UI from scratch on every frame — Paper handles the rest. This page explains what that means in practice, why it is powerful, and how Paper keeps things efficient behind the scenes.

The Frame Loop

Every frame, your code calls BeginFrame, declares all the elements you want to see, then calls EndFrame. Paper collects those declarations, runs layout, and renders the result.
void RenderUI()
{
    paper.BeginFrame(deltaTime);

    // Declare your entire UI here — every frame
    using (paper.Column("Root").Enter())
    {
        paper.Box("Header").Height(60).BackgroundColor(Color.DarkBlue);
        paper.Box("Body").Height(paper.Stretch());
    }

    paper.EndFrame();
}
BeginFrame resets the internal element tree. EndFrame runs layout, triggers interactions, and renders — in that order.
Nothing you declare survives past EndFrame as a live object. The element tree is rebuilt completely the next time BeginFrame runs.

Retained Mode vs. Immediate Mode

To understand why this matters, compare the two approaches with a simple toggle button.
// You create the widget once, store it, and mutate it later
var button = new Button("Dark Mode");
button.OnClick += () => isDarkMode = !isDarkMode;
ui.Add(button);

// Later, somewhere else, you update it:
button.Text = isDarkMode ? "Light Mode" : "Dark Mode";
button.BackgroundColor = isDarkMode ? Color.Dark : Color.Light;
In the retained approach, you must keep widget references in sync with your application state. In Paper, your application state is the UI — you simply read it during the frame. There is no divergence to manage.

How Element Identity Works

Because the element tree is rebuilt every frame, Paper needs a stable way to match this frame’s elements to last frame’s elements. This is how animated transitions, hover states, and per-element storage all survive across frames. Every element gets a deterministic integer ID computed from four ingredients:
int storageHash = HashCode.Combine(
    CurrentParent.Data.ID,  // parent's ID  — scopes the ID to the subtree
    _IDStack.Peek(),        // active PushID scope  — scopes inside loops
    stringID,              // the string you pass to Box/Row/Column
    intID,                 // optional integer you pass (useful in loops)
    lineID                 // [CallerLineNumber] — automatically injected
);
This means two different call sites that happen to pass the same stringID still get distinct IDs because lineID differs. An element created inside a for loop needs the loop index passed as intID (or a PushID scope) to stay distinct across iterations.
If two elements in the same parent resolve to the same hash, Paper throws an exception: "Element already exists with this ID". The most common cause is creating elements in a loop without passing a unique intID or using PushID.

Persisting State Across Frames

IMGUI elements don’t exist between frames, so any state that needs to survive — scroll position, open/closed flag, animation progress — must be stored explicitly. Paper provides per-element storage keyed by your element’s stable ID:
var panelBuilder = paper.Column("ExpandablePanel");
var panelHandle = panelBuilder._handle;   // capture handle before Enter()

using (panelBuilder.Enter())
{
    // Read persisted state for this element (defaults to false on first frame).
    // CurrentParent is "ExpandablePanel" here, so no handle needed.
    bool isOpen = paper.GetElementStorage<bool>("isOpen");

    paper.Box("Header")
        .Height(40)
        .BackgroundColor(Color.Gray)
        .Text(isOpen ? "▼ Collapse" : "► Expand", myFont)
        // Pass the captured handle explicitly — CurrentParent is Root during event callbacks.
        .OnClick(_ => paper.SetElementStorage(panelHandle, "isOpen", !isOpen));

    if (isOpen)
    {
        paper.Box("Content")
            .Height(120)
            .BackgroundColor(Color.LightGray);
    }
}
GetElementStorage<T> and SetElementStorage<T> have overloads that accept an explicit ElementHandle. During event callbacks such as OnClick, CurrentParent has already returned to the root element — always capture the handle before entering the scope and pass it explicitly inside callbacks. Storage entries whose element was not created in the last frame are automatically cleaned up by EndFrame.
Storage keys are plain strings — namespace them descriptively (e.g. "scrollY", "isOpen") to avoid accidental collisions between components that share the same element ID.
There is also a root-level store for truly global state:
// Survives across all frames and is not tied to any element
paper.SetRootStorage("globalFlag", true);
bool flag = paper.GetRootStorage<bool>("globalFlag");

When to Use Immediate Mode

IMGUI shines for:
  • Game UIs and tooling — content changes frequently and closely follows application state.
  • Developer tools — property inspectors, debuggers, level editors.
  • Dynamic lists and data grids — add or remove rows without managing widget lifecycles.
  • Prototyping — iterate quickly without architecture ceremony.
Retained-mode frameworks are a better fit when:
  • The UI is largely static and driven by data binding infrastructure (e.g. MVVM).
  • Accessibility trees or native widget semantics are a hard requirement.
  • You need deep OS integration (system dialogs, native menus).
Paper sits comfortably in the game engine / interactive tool space where IMGUI’s directness is a significant advantage.

Build docs developers (and LLMs) love