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.

Paper organises its UI as a tree of elements. Every frame that tree is built from scratch: you call Box, Row, or Column, optionally step inside them with Enter(), and Paper assembles the parent-child relationships automatically through an internal stack. This page explains exactly how that stack works, what an ElementHandle is, and the tools available for managing identity in complex scenarios.

The Three Element Primitives

All elements in Paper are created through three methods on Paper:
MethodWhat it creates
paper.Box(id)A generic container with no default layout direction (inherits Column)
paper.Row(id)A container that arranges its children horizontally
paper.Column(id)A container that arranges its children vertically
Row and Column are simply Box with .LayoutType(LayoutType.Row) or .LayoutType(LayoutType.Column) pre-applied. All three return an ElementBuilder you can chain style and event methods on.
// A plain leaf element — no children
paper.Box("MyButton")
    .Size(120, 40)
    .BackgroundColor(Color.ForestGreen)
    .Rounded(8)
    .OnClick(_ => Console.WriteLine("Clicked!"));

// A horizontal container
paper.Row("Toolbar")
    .Height(48)
    .BackgroundColor(Color.DarkGray);

// A vertical container
paper.Column("Sidebar")
    .Width(200)
    .BackgroundColor(Color.LightGray);

Establishing Parent–Child Relationships with Enter()

Calling .Enter() on an ElementBuilder returns an IDisposable that pushes the element onto the internal element stack. Any element created inside the using block is automatically added as a child of the entered element. When the using block ends, the element is popped off the stack.
using (paper.Column("App")
    .BackgroundColor(Color.White)
    .Enter())
{
    // These three elements become children of "App"
    using (paper.Row("Header").Height(60).Enter())
    {
        paper.Box("Logo").Size(40).BackgroundColor(Color.Blue);
        paper.Box("Title").Width(paper.Stretch()).Text("My App", myFont);
    }

    using (paper.Row("Body").Height(paper.Stretch()).Enter())
    {
        paper.Box("Nav").Width(200).BackgroundColor(Color.LightGray);
        paper.Box("Content").Width(paper.Stretch());
    }
}
The using pattern mirrors how nesting works visually. Indentation level equals depth in the element tree.

The Element Stack

Internally, Paper maintains a Stack<ElementHandle> called _elementStack. Its top item is always accessible as paper.CurrentParent. When you call paper.Box(...), the new element is immediately added as a child of CurrentParent. When .Enter() is called, that new element is pushed onto the stack so subsequent elements become its children.
Frame start:
  Stack: [ Root ]          ← CurrentParent = Root

After Column("App").Enter():
  Stack: [ Root, App ]     ← CurrentParent = App

After Row("Header").Enter():
  Stack: [ Root, App, Header ] ← CurrentParent = Header

End of Header's using block:
  Stack: [ Root, App ]     ← CurrentParent = App again
The root element is always on the bottom of the stack and is never popped. It covers the full screen area and is the implicit parent of any top-level elements you create.

ElementHandle: a Lightweight Reference

ElementHandle is a readonly struct that identifies a single element. It holds a reference to the owning Paper instance and an integer index into the element array. You can use it to access the element’s layout data after the frame, pass it to storage APIs, or add custom render actions.
// Capture the handle at declaration time — grab it from the builder before calling Enter()
var panelBuilder = paper.Column("Panel").BackgroundColor(Color.Gray);
ElementHandle myPanel = panelBuilder._handle;

using (panelBuilder.Enter())
{
    paper.Box("Child").Size(50).BackgroundColor(Color.Blue);
}

// Use the handle to read layout data after layout runs
paper.OnEndOfFramePostLayout += () =>
{
    var rect = new Rect(myPanel.Data.X, myPanel.Data.Y,
                        myPanel.Data.X + myPanel.Data.LayoutWidth,
                        myPanel.Data.Y + myPanel.Data.LayoutHeight);
    Console.WriteLine($"Panel is at {rect}");
};
ElementHandle.IsValid returns true when the handle refers to a live element in the current frame’s array. Accessing handle.Data on an invalid handle will throw.
// Navigate upward through the tree
ElementHandle parent = myPanel.GetParentHandle();
if (parent.IsValid)
    Console.WriteLine($"Parent ID: {parent.Data.ID}");

Moving Elements to the Root with MoveToRoot()

Popups, tooltips, and modals should render above all other content regardless of where they are declared in code. MoveToRoot() detaches the current element from its existing parent and re-attaches it as a direct child of the root element.
using (paper.Column("Page").Enter())
{
    paper.Box("NormalContent").Height(200);

    // This popup is declared inside "Page" but will render at root level
    using (paper.Box("Popup")
        .Size(300, 150)
        .BackgroundColor(Color.White)
        .Layer(Layer.Overlay)
        .Enter())
    {
        paper.MoveToRoot();   // re-parent to root, renders on top of everything
        paper.Box("PopupBody").Text("Hello from popup!", myFont);
    }
}
MoveToRoot() operates on CurrentParent at the point it is called — call it immediately after .Enter(), before adding children, so those children are scoped under the re-parented element.

Scoping IDs in Loops with PushID / PopID

Element IDs are hashed from the string ID, the call-site line number, and the parent’s ID. When you create elements inside a loop, multiple iterations execute the same line with the same string — producing duplicate hashes and a thrown exception. The solution is to either pass the loop index as intID:
for (int i = 0; i < items.Count; i++)
{
    // intID differentiates each iteration
    paper.Box("Item", i)
        .Height(40)
        .Text(items[i].Name, myFont);
}
Or use PushID / PopID to create a new ID scope:
foreach (var item in items)
{
    paper.PushID(item.UniqueId);   // push a scope unique to this item

    paper.Box("Row")
        .Height(40)
        .Text(item.Name, myFont);

    paper.Box("Delete")
        .Size(24)
        .OnClick(_ => RemoveItem(item));

    paper.PopID();                 // restore the previous scope
}
PushID(string) and PushID(int) both combine with the current top of the ID stack, so scopes nest correctly. Always pair every PushID with a PopID.
Forgetting PopID will corrupt all element IDs created after it in the same frame. If you see unexpected “Element already exists” errors, a missing PopID is the most likely cause.

How the Tree Is Cleaned Up Each Frame

At the start of BeginFrame, Paper calls ClearElements() and rebuilds the root. The _createdElements hash set tracks every element hash registered during the frame. At the end of EndFrame, EndOfFrameCleanupStorage() iterates over all stored per-element data and removes entries whose ID was not present in _createdElements — meaning the element was not declared this frame and therefore no longer exists. This automatic cleanup means you never need to explicitly destroy elements. Simply stop declaring them and their storage evaporates on the next frame.

Build docs developers (and LLMs) love