Skip to content

Nesting Layouts

Complex terminal UIs are built by nesting layouts within layouts. This compositional approach lets you create sophisticated interfaces from simple building blocks.

Basic Nesting

Layouts can contain other layouts as children:

csharp
Layouts.Vertical()
    .WithChild(header.Height(1))
    .WithChild(
        // Nested horizontal layout for content area
        Layouts.Horizontal()
            .WithChild(sidebar.Width(25))
            .WithChild(mainContent.WidthFill())
            .Fill())  // This nested layout needs Fill() to expand vertically
    .WithChild(footer.Height(1));

Result:

┌─────────────────────────────────────────┐
│ Header                                   │  ← 1 row
├────────────┬────────────────────────────┤
│            │                            │
│  Sidebar   │      Main Content          │  ← Fill
│            │                            │
├────────────┴────────────────────────────┤
│ Footer                                   │  ← 1 row
└─────────────────────────────────────────┘

Dashboard Grid Pattern

Create a grid by nesting horizontal layouts in a vertical container:

csharp
Layouts.Vertical()
    .WithChild(
        Layouts.Horizontal()
            .WithChild(topLeft.WidthFill())
            .WithChild(topRight.WidthFill())
            .Fill())  // Row expands to fill available height
    .WithChild(
        Layouts.Horizontal()
            .WithChild(bottomLeft.WidthFill())
            .WithChild(bottomRight.WidthFill())
            .Fill());  // Row expands to fill available height

Result:

┌───────────────────┬───────────────────┐
│                   │                   │
│    Top Left       │    Top Right      │
│                   │                   │
├───────────────────┼───────────────────┤
│                   │                   │
│   Bottom Left     │   Bottom Right    │
│                   │                   │
└───────────────────┴───────────────────┘

Three-Tier Application Layout

A common pattern for full applications:

csharp
protected override ILayoutNode BuildLayout()
{
    return Layouts.Vertical()
        // Tier 1: Header bar
        .WithChild(BuildHeader().Height(1))

        // Tier 2: Main content area
        .WithChild(
            Layouts.Horizontal()
                .WithChild(BuildNavigation().Width(20))
                .WithChild(BuildContentArea().WidthFill())
                .WithChild(BuildDetailsPanel().Width(30))
                .Fill())  // Content row fills available height

        // Tier 3: Status bar
        .WithChild(BuildStatusBar().Height(1));
}

private ILayoutNode BuildHeader()
{
    return Layouts.Horizontal()
        .WithChild(new TextNode("MyApp").Bold().WidthAuto())
        .WithChild(new EmptyNode().WidthFill())
        .WithChild(new TextNode($"User: {username}").WidthAuto());
}

private ILayoutNode BuildStatusBar()
{
    return Layouts.Horizontal()
        .WithChild(statusText.WidthFill())
        .WithChild(new TextNode("[F1] Help [Esc] Quit").WidthAuto());
}

Panels with Internal Layout

PanelNode accepts content that can itself be a layout:

csharp
new PanelNode()
    .WithTitle("User Details")
    .WithBorder(BorderStyle.Rounded)
    .WithContent(
        Layouts.Vertical()
            .WithChild(new TextNode($"Name: {user.Name}"))
            .WithChild(new TextNode($"Email: {user.Email}"))
            .WithChild(new TextNode($"Role: {user.Role}")))
    .Height(6);

Extracting Reusable Components

For complex UIs, extract nested layouts into methods:

csharp
public class DashboardPage : ReactivePage<DashboardViewModel>
{
    protected override ILayoutNode BuildLayout()
    {
        return Layouts.Vertical()
            .WithChild(BuildHeader().Height(3))
            .WithChild(BuildMainArea().Fill())  // Main area takes remaining space
            .WithChild(BuildFooter().Height(1));
    }

    private ILayoutNode BuildHeader()
    {
        return new PanelNode()
            .WithTitle("Dashboard")
            .WithBorder(BorderStyle.Double)
            .WithContent(new TextNode("Welcome!"));
    }

    private ILayoutNode BuildMainArea()
    {
        return Layouts.Horizontal()
            .WithChild(BuildMetricsPanel().WidthFill(weight: 2))
            .WithChild(BuildActivityPanel().WidthFill(weight: 1));
    }

    private ILayoutNode BuildMetricsPanel()
    {
        return new PanelNode()
            .WithTitle("Metrics")
            .WithContent(
                ViewModel.MetricsChanged
                    .Select(m => new TextNode(FormatMetrics(m)))
                    .AsLayout());
    }

    private ILayoutNode BuildActivityPanel()
    {
        return new PanelNode()
            .WithTitle("Activity")
            .WithContent(
                ViewModel.ActivityChanged
                    .Select(a => new TextNode(FormatActivity(a)))
                    .AsLayout());
    }

    private ILayoutNode BuildFooter()
    {
        return new TextNode("[Q] Quit [R] Refresh")
            .WithForeground(Color.Gray);
    }
}

Nesting with Reactive Content

When nesting reactive content, be mindful of where the reactive boundary is:

csharp
// Good: Reactive content inside fixed structure
Layouts.Vertical()
    .WithChild(
        new PanelNode()
            .WithTitle("Status")
            .WithContent(
                ViewModel.StatusChanged
                    .Select(s => new TextNode(s))
                    .AsLayout())
            .Height(3))
    .WithChild(content.Fill());

// Bad: Recreating entire structure reactively (inefficient)
ViewModel.StatusChanged
    .Select(s =>
        Layouts.Vertical()
            .WithChild(new TextNode(s).Height(3))
            .WithChild(content.Fill()))
    .AsLayout();

StackLayout for Overlays

Use StackLayout when you need to overlay content:

csharp
new StackLayout(new[]
{
    // Bottom layer: main content
    mainContent,

    // Top layer: modal dialog (rendered on top)
    ViewModel.ShowModalChanged
        .Select(show => show
            ? BuildModalDialog()
            : (ILayoutNode)new EmptyNode())
        .AsLayout()
});

Performance Considerations

  1. Keep reactive boundaries small - Only wrap the parts that need to update
  2. Avoid deep nesting - Flatten where possible for better performance
  3. Cache static layouts - If a layout doesn't change, consider making it a field
  4. Use EmptyNode for placeholders - Not null, which would break the tree
csharp
// Efficient: only the text updates
new PanelNode()
    .WithContent(
        ViewModel.CountChanged
            .Select(c => new TextNode($"Count: {c}"))
            .AsLayout());

// Less efficient: entire panel recreated on each update
ViewModel.CountChanged
    .Select(c => new PanelNode().WithContent(new TextNode($"Count: {c}")))
    .AsLayout();

Released under the Apache 2.0 License.