💡Core Concepts

Now that you've got a basic UI running, let's explore the fundamental concepts. Understanding these ideas will unlock the library's full potential and make building complex UIs easy.

As we covered briefly in "Getting Started," Paper is an immediate-mode UI library. This means you define the entire UI every single frame. The library doesn't hold onto a persistent tree of "widget" objects that you need to find and modify later.

  • Your code is the UI definition. The structure of your code directly maps to the structure of the UI on the screen.

  • State is managed by you. If you want a button to change color when a variable is true, you simply check that variable every frame and set the color accordingly.

  • Simplicity is key. This approach eliminates complex event listeners, data binding, and the need to manage object references, which simplifies your application logic.

The Frame Lifecycle: BeginFrame() and EndFrame()

Every piece of UI you create in Paper exists within a single frame, bracketed by BeginFrame() and EndFrame(). Here’s what happens under the hood during one full cycle:

  1. Paper.BeginFrame(deltaTime):

    • This call prepares Paper for a new UI definition.

    • It clears out all the element data from the previous frame.

    • It sets up a clean slate with a single "root" element that fills the entire screen.

    • It processes any user input that has occurred since the last frame.

  2. Your UI Code:

    • Between BeginFrame() and EndFrame(), you call methods like Paper.Box(), Paper.Row(), etc., to define your UI elements.

    • As you define elements, Paper builds a hierarchy or "tree" of them in the background.

  3. Paper.EndFrame():

    • This is where the magic happens. Once all elements for the frame have been defined, EndFrame() performs several crucial steps:

      • Layout Calculation: It runs a powerful layout engine that calculates the final size and position of every single element based on the rules you've defined (like Width, Margin, Stretch, etc.).

      • Interaction Handling: It determines which elements are being hovered over, clicked, or dragged.

      • Rendering: It translates your UI definition into a series of drawing commands (like "draw rounded rectangle here," "draw text there") and sends them to the renderer you provided during initialization.

Element Hierarchy & The using Pattern

UI is naturally hierarchical. A button might be inside a panel, which is inside a window. Paper makes managing this hierarchy simple and visual through the use of C#'s using statement.

When you create an element that can contain other elements (like a Row, Column, or Box), you can call .Enter() on it within a using block.

using (paper.Column("MyContainer").Enter())
{
    // Any element created here is a child of "MyContainer"
    paper.Box("Header");
    paper.Box("Content");
    paper.Box("Footer");
}
  • When the using block is entered, the new container (MyContainer in this case) becomes the "current parent."

  • Any elements defined inside the block are automatically added as its children.

  • When the using block is exited, the previous element automatically becomes the "current parent" again.

You can nest these blocks to create complex layouts that are easy to read and manage:

using (paper.Column("MainContainer").BackgroundColor(Color.LightGray).Enter())
{
    // This is a child of "MainContainer"
    using (paper.Row("Header").Height(60).BackgroundColor(Color.DarkBlue).Enter())
    {
        // This is a child of "Header"
        paper.Box("Logo").Size(60).BackgroundColor(System.Drawing.Color.IndianRed);
    } // "MainContainer" is the parent again here

    // This is another child of "MainContainer"
    paper.Box("ContentArea");
}

This code creates the following structure:

  • MainContainer (Column)

    • Header (Row)

      • Logo (Box)

    • ContentArea (Box)

Unique Element IDs

You might be wondering: if all elements are recreated every frame, how can the UI remember state, like which button is being hovered or the progress of an animation?

The answer is stable, unique IDs.

When you create an element, Paper generates a unique internal ID for it. This ID is what allows Paper to track an element's state from one frame to the next. This ID is automatically generated by combining three pieces of information:

  1. The Parent's ID: Which container the element lives in.

  2. The String ID: The name you provide, like "MyButton".

  3. The Source Line Number: The physical line in your .cs file where you call the method (e.g., Paper.Box()). The C# compiler automatically passes this in for you.

This composite ID acts as a stable key to a dictionary where Paper can store stateful information about that element, such as its hover state, click state, or current animation values.

Practical Examples

Let's see how this works in practice.

✅ This is perfectly valid:

Because each call to Paper.Box() is on a different line of code, their generated IDs will be unique, even though their string IDs and parents are the same.

// Line 101
paper.Box("MyButton").Text(Text.Center("Button 1", myFont, Color.White));
// Line 102
paper.Box("MyButton").Text(Text.Center("Button 2", myFont, Color.White));
// Line 103
paper.Box("MyButton").Text(Text.Center("Button 3", myFont, Color.White));

❌ This will cause an ID collision:

When creating elements in a loop, each iteration happens on the exact same line of code. If you use the same string ID, the generated ID will be identical for every element, causing an error.

for (int i = 0; i < 5; i++)
{
    // Every button here has the same parent, same string "MyButton",
    // and is created on the same line of code. This will fail!
    paper.Box("MyButton").Text(Text.Center($"Button", myFont, Color.White));
}

✅ This is the correct way to create elements in a loop:

To fix the loop, you must make the generated ID unique for each iteration. The most common way is to pass in the loop variable.

for (int i = 0; i < 5; i++)
{
    // By including 'i', we ensure the final
    // generated ID is unique for each button in the loop.
    paper.Box("MyButton", i).Text(Text.Center("Button", myFont, Color.White));
}

You could also append it to the string instead of passing it as a parameter $"MyButton_{i}" This is slower but can be more reliable in some cases.

Understanding how these IDs are generated is key to building dynamic and complex UIs without running into collisions. In the next section, we'll dive deep into the Layout Engine.

Last updated