Skip to content
Subha Sundar Das edited this page Aug 12, 2026 · 2 revisions

Retui Layout System

A parent determines how much space is available. Children describe how they want to use that space. The layout engine calculates the final size and position of every child.


Table of Contents



1. The Core Mental Model

A Retui UI is a tree of elements:

Screen
└── Box
    ├── Header
    ├── Content
    └── Footer

Layout flows top-down from parent to children:

Parent
  │
  │  Available space (Rect)
  ▼
Child
  │
  │  Final allocated Rect
  ▼
Render

Every element receives a concrete Rect from its parent:

type Rect struct {
    X      int
    Y      int
    Width  int
    Height int
}

A child never needs to inspect its parent. The parent calculates the available space and hands each child a concrete rectangle.

Parent decides the space. Child uses the space.


2. Parent → Child Layout

The conversation between a parent and its children looks like this:

Parent:
"I have 80 columns and 40 rows."
          ↓
Child A:
"I need exactly 5 rows."
          ↓
Child B:
"Give me whatever remains."
          ↓
Parent:
"Child A gets 80 × 5. Child B gets 80 × 35."

Each child is responsible for rendering itself inside the rectangle it receives — nothing more.


3. Sizing Modes

Retui provides four sizing modes for both width and height:

Mode Meaning
Fixed(n) Exactly n cells
Fit() Natural content size
Grow(n) Proportional share of remaining space
Percent(n) Percentage of parent's resolved size

3.1 Fixed

Fixed(n) allocates exactly n cells on that axis, regardless of content or parent size.

retui.Box(
    retui.Props{
        Width:  retui.Fixed(80),
        Height: retui.Fixed(5),
    },
    style,
    content,
)
┌────────────────────────────────────────────────────────────────────────────────┐
│                                                                                │
│                              content                                           │
│                                                                                │
│                                                                                │
│                                                                                │
└────────────────────────────────────────────────────────────────────────────────┘
← 80 columns →
↕ 5 rows

When to use Fixed:

  • Header and footer rows
  • Fixed-width sidebars
  • Modal dialogs
  • Status bars
  • Any element that must be a predictable size

3.2 Fit

Fit() sizes the element to its natural content dimensions. No more, no less.

retui.Box(
    retui.Props{
        Direction: retui.Column,
        Height:    retui.Fit(),
    },
    style,
    retui.Text("Name:  Alice"),
    retui.Text("Email: alice@example.com"),
)

Both text rows are 1 line each → the box height resolves to 2:

┌──────────────────────────┐
│ Name:  Alice             │
│ Email: alice@example.com │
└──────────────────────────┘
↕ 2 rows (natural content size)

When to use Fit:

  • Labels and captions
  • Forms that grow with their fields
  • Tooltips and small panels
  • Any element whose size should be driven by its content

3.3 Grow

Grow(n) takes a proportional share of the remaining space after Fixed and Fit children are resolved.

retui.Box(
    retui.Props{
        Direction: retui.Column,
        Height:    retui.Fixed(40),
    },
    style,
    header,                                     // Fixed(4)
    retui.Box(
        retui.Props{Height: retui.Grow(1)},
        style,
        content,                                // gets remaining 33 rows
    ),
    footer,                                     // Fixed(3)
)

The engine calculates:

Total:   40
Header:   4  (Fixed)
Footer:   3  (Fixed)
         ──
Used:    7
         ──
Content: 33  (Grow(1) → all remaining)

Result:

┌────────────────────────────────────┐
│ Header                             │  4
├────────────────────────────────────┤
│                                    │
│                                    │
│              Content               │  33
│                                    │
│                                    │
├────────────────────────────────────┤
│ Footer                             │  3
└────────────────────────────────────┘

Grow is a request, not a command. The parent calculates remaining space; the child receives it.

When to use Grow:

  • Main content area
  • Flexible columns in a row
  • Any element that should fill available space

3.4 Multiple Grow Children

When multiple children use Grow, remaining space is divided by their combined weight.

retui.Box(
    retui.Props{
        Direction: retui.Row,
        Width:     retui.Fixed(90),
    },
    style,
    retui.Box(retui.Props{Width: retui.Grow(1)}, style, panelA), // 30
    retui.Box(retui.Props{Width: retui.Grow(2)}, style, panelB), // 60
)
Total available: 90
Weight total:     3   (1 + 2)

Panel A = 90 × 1/3 = 30
Panel B = 90 × 2/3 = 60
┌──────────────┬───────────────────────────────┐
│              │                               │
│   Panel A    │           Panel B             │
│   (Grow 1)   │           (Grow 2)            │
│              │                               │
└──────────────┴───────────────────────────────┘
← 30 ──────────← 60 ────────────────────────────

3.5 Percent

Percent(n) allocates a fraction of the parent's resolved size on that axis.

retui.Box(
    retui.Props{
        Direction: retui.Row,
        Width:     retui.Fixed(80),
    },
    style,
    retui.Box(retui.Props{Width: retui.Percent(30)}, style, leftPanel),  // 24
    retui.Box(retui.Props{Width: retui.Percent(70)}, style, rightPanel), // 56
)
Parent width: 80

Left  = 80 × 30% = 24
Right = 80 × 70% = 56

When to use Percent:

  • Split panels (50/50, 30/70)
  • Two-column forms
  • Proportional sidebars

Note: Unlike Grow, Percent depends on the parent's resolved size being known before children are laid out. For most cases Grow is simpler and more predictable.


4. Spacing

4.1 Padding

Padding is space inside the parent. It reduces the area available to children.

retui.Box(
    retui.Props{
        Padding: [4]int{1, 2, 1, 2}, // Top, Right, Bottom, Left
    },
    style,
    content,
)
Parent: 80 × 40

Padding:  T=1  R=2  B=1  L=2

Children receive:
  Width  = 80 - 2 - 2 = 76
  Height = 40 - 1 - 1 = 38
┌────────────────────────────────────┐
│  padding top                       │  ← 1 row
│  ┌──────────────────────────────┐  │
│  │                              │  │
│  │         content              │  │  76 wide
│  │                              │  │
│  └──────────────────────────────┘  │
│  padding bottom                    │  ← 1 row
└────────────────────────────────────┘
 ↑                                ↑
 L=2                              R=2

4.2 Margin

Margin is space outside the child. It reduces the space the child occupies in its parent's layout without reducing the child's own inner dimensions.

Space location Affects
Padding Inside the parent Space available to children
Margin Outside the child Space the child takes in the parent
┌───────────────────────────────┐  Parent
│                               │
│       margin                  │
│       ┌─────────────────┐     │
│       │     content     │     │  Child
│       └─────────────────┘     │
│                               │
└───────────────────────────────┘

4.3 Gap

Gap inserts uniform space between siblings. It is set on the parent, not the children.

retui.Box(
    retui.Props{
        Direction: retui.Column,
        Gap:       1,
    },
    style,
    row1, row2, row3,
)
┌───────────────────────────┐
│ Row 1                     │
│                           │  ← gap (1 row)
│ Row 2                     │
│                           │  ← gap (1 row)
│ Row 3                     │
└───────────────────────────┘

Use Gap for spacing between siblings. Avoid inserting empty Box elements manually.


5. Direction

Retui supports two layout directions:

Row

Children are arranged horizontally (left to right).

Main axis  → X (horizontal)
Cross axis → Y (vertical)

┌────────┬────────┬────────┐
│ Child1 │ Child2 │ Child3 │
└────────┴────────┴────────┘

Column

Children are arranged vertically (top to bottom).

Main axis  → Y (vertical)
Cross axis → X (horizontal)

┌───────────────┐
│ Child 1       │
├───────────────┤
│ Child 2       │
├───────────────┤
│ Child 3       │
└───────────────┘

The distinction between Row and Column is critical when using Align and Justify.


6. Alignment

Align positions children on the cross axis (perpendicular to Direction).

For a Column, the cross axis is horizontal:

Value Result
AlignStart Children anchored to the left
AlignCenter Children centered horizontally
AlignEnd Children anchored to the right
AlignStretch Children fill available width
AlignStart              AlignCenter             AlignEnd
┌───────────────────┐   ┌───────────────────┐   ┌───────────────────┐
│ ████              │   │      ████         │   │              ████ │
│ ████              │   │      ████         │   │              ████ │
└───────────────────┘   └───────────────────┘   └───────────────────┘

Align controls the cross axis. For Column, that is horizontal. For Row, that is vertical.


7. Justify

Justify distributes children along the main axis (the direction of layout).

For a Column, the main axis is vertical:

Value Result
JustifyStart Children packed at the top
JustifyEnd Children packed at the bottom
JustifyCenter Children centered vertically
JustifySpaceBetween Free space distributed between children
JustifySpaceAround Free space distributed around children
JustifyStart           JustifyCenter          JustifySpaceBetween
┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│ Child 1      │       │              │       │ Child 1      │
│ Child 2      │       │ Child 1      │       │              │
│ Child 3      │       │ Child 2      │       │              │
│              │       │ Child 3      │       │ Child 2      │
│              │       │              │       │              │
└──────────────┘       └──────────────┘       │              │
                                              │ Child 3      │
                                              └──────────────┘

Justify controls the main axis. For Column, that is vertical. For Row, that is horizontal.


8. Overflow and Scrolling

When content is larger than its container, Overflow controls the behaviour:

Value Behaviour
OverflowVisible Content renders beyond the boundary
OverflowHidden Content is clipped at the boundary
OverflowScroll A viewport is created; content scrolls

With scrolling, the content may be much taller than the visible area:

Content height: 100 rows
Viewport:        20 rows

┌────────────────────────┐
│ Ledger 3               │  ← scroll position: row 3
│ Ledger 4               │
│ Ledger 5               │
│ Ledger 6               │
│ Ledger 7               │
│ ...                    │
└────────────────────────┘  viewport = 20 rows

The scroll offset determines which portion of the full content is visible.


9. Wrap

In a Row, Wrap causes children to move to the next line when they no longer fit horizontally.

Without wrap (content overflows):

Available width: 30

┌──────────────────────────────┐
│ AAAAAAAAAA BBBBBBBBBB CCCCC… │  ← overflow
└──────────────────────────────┘

With wrap:

┌──────────────────────────────┐
│ AAAAAAAAAA BBBBBBBBBB        │
│ CCCCCCCCCC DDDDDDDDDD        │
└──────────────────────────────┘

The layout engine automatically calculates the resulting height based on how many lines are needed.


10. Spacer

retui.Spacer() is a convenience element that expands to fill all remaining space on both axes:

retui.Spacer()
// equivalent to:
// Width:  Grow(1)
// Height: Grow(1)

Common pattern — push an element to the bottom:

retui.Box(
    retui.Props{
        Direction: retui.Column,
        Height:    retui.Fixed(40),
    },
    style,
    retui.Text("Title"),
    retui.Spacer(),            // absorbs all remaining rows
    retui.Text("Press Q to quit"),
)
┌────────────────────────┐
│ Title                  │
│                        │
│                        │
│                        │  ← Spacer fills this area
│                        │
│ Press Q to quit        │
└────────────────────────┘

No manual coordinate calculation required.


11. The Layout Pipeline

The engine resolves the element tree in a single top-down pass:

           Parent (Rect known)
                │
                │  Passes available space
                ▼
        ┌───────────────┐
        │  Measure pass │  ← resolve Fixed, Fit, Percent
        └───────────────┘
                │
                ▼
        ┌───────────────┐
        │  Grow pass    │  ← distribute remaining to Grow children
        └───────────────┘
                │
                ▼
        ┌───────────────┐
        │  Position     │  ← apply Align, Justify, Gap, Padding
        └───────────────┘
                │
                ▼
        Child Rect assigned
                │
                ▼
        Layout child's children
                │
                ▼
              Render

The key insight: layout flows from outside to inside. Every element receives a concrete rectangle before it lays out its own children.

Screen → Box → Header → Text
                └────→ Content → List → Row → Cell
                └────→ Footer → Text

12. A Complete Example

A typical ERP screen with header, sidebar, content, and footer:

retui.Box(
    retui.Props{
        Direction: retui.Column,
        Width:     retui.Percent(100),
        Height:    retui.Percent(100),
    },
    style,

    // ── Header ────────────────────────────────────────────────
    retui.Box(
        retui.Props{Height: retui.Fixed(3)},
        style,
        headerElement,
    ),

    // ── Main area ─────────────────────────────────────────────
    retui.Box(
        retui.Props{
            Direction: retui.Row,
            Height:    retui.Grow(1),
            Gap:       1,
        },
        style,

        // Sidebar
        retui.Box(
            retui.Props{Width: retui.Fixed(20)},
            style,
            sidebarElement,
        ),

        // Content
        retui.Box(
            retui.Props{Width: retui.Grow(1)},
            style,
            contentElement,
        ),
    ),

    // ── Footer ────────────────────────────────────────────────
    retui.Box(
        retui.Props{Height: retui.Fixed(2)},
        style,
        footerElement,
    ),
)

The engine resolves it in order:

Screen: 80 × 40
│
├── Header    Fixed(3)  → 80 × 3
│
├── Main      Grow(1)   → 80 × 35
│   │
│   ├── Sidebar   Fixed(20) → 20 × 35
│   │
│   └── Content   Grow(1)   → 59 × 35   (80 - 20 - gap:1)
│
└── Footer    Fixed(2)  → 80 × 2

Visual result:

┌────────────────────────────────────────────────────────────────────────────────┐
│  Header                                                                        │  3
├───────────────────┬────────────────────────────────────────────────────────────┤
│                   │                                                            │
│                   │                                                            │
│   Sidebar         │                    Content                                 │  35
│   20 cols         │                    59 cols                                 │
│                   │                                                            │
├───────────────────┴────────────────────────────────────────────────────────────┤
│  Footer                                                                        │  2
└────────────────────────────────────────────────────────────────────────────────┘

No component calculated its own position. Every element received a concrete Rect from its parent.


13. Component Reusability

Because children never inspect their parent, the same component works in any context:

func LedgerSelect() retui.Element { ... }

This component works unchanged whether placed inside a:

Form  →  Dialog  →  Sidebar  →  Full-screen view

The parent controls how much space LedgerSelect receives. The component renders inside whatever rectangle it is given.

Write components that avoid hard-coded assumptions:

// ❌ Avoid — assumes the parent is always 80 columns wide
retui.Box(retui.Props{Width: retui.Fixed(78)}, style, content)

// ✅ Prefer — fills whatever space the parent provides
retui.Box(retui.Props{Width: retui.Grow(1)}, style, content)

14. Decision Checklist

When laying out a component, answer these questions in order:

Sizing

  1. Should the element have an exact size? → Fixed(n)
  2. Should the element size itself from content? → Fit()
  3. Should the element fill remaining space? → Grow(n)
  4. Should the element be proportional to its parent? → Percent(n)

Direction and spacing

  1. Should children be horizontal or vertical? → Direction: Row / Column
  2. How much space between siblings? → Gap
  3. How much inner breathing room? → Padding
  4. How much outer breathing room? → Margin

Alignment

  1. How should children align on the cross axis? → Align
  2. How should children distribute on the main axis? → Justify

Edge cases

  1. What happens if content is too tall/wide? → Overflow
  2. Should rows wrap when they run out of width? → Wrap

15. Quick Reference

Concept Purpose Common values
Fixed(n) Exact size Fixed(40), Fixed(80)
Fit() Natural content size Fit()
Grow(n) Remaining space by weight Grow(1), Grow(2)
Percent(n) Proportional to parent Percent(50), Percent(30)
Padding Space inside parent [4]int{1, 2, 1, 2} (T R B L)
Margin Space outside child [4]int{0, 1, 0, 1}
Gap Space between siblings Gap: 1, Gap: 2
Direction Layout axis Row, Column
Align Cross-axis positioning AlignStart, AlignCenter, AlignEnd, AlignStretch
Justify Main-axis distribution JustifyStart, JustifyCenter, JustifySpaceBetween
Overflow Content outside bounds OverflowVisible, OverflowHidden, OverflowScroll
Wrap Line wrapping in Row Wrap: true

16. Layout Rules

# Rule
1 Parent owns available space. The parent receives a rectangle and distributes it among children.
2 Children do not inspect their parent. Children receive their rectangle from the layout engine.
3 Fixed is resolved first. Fixed-size children claim their space before others.
4 Fit depends on content. Fit-sized elements measure their children first.
5 Grow receives what remains. After Fixed and Fit are resolved, remaining space goes to Grow children.
6 Grow weights are proportional. Grow(1) and Grow(2) split remaining space in a 1:2 ratio.
7 Percent depends on the parent's resolved size. The parent must be resolved before Percent children are calculated.
8 Layout is recursive. Each element lays out its children using the rectangle it received.
9 Align uses the cross axis. Perpendicular to Direction.
10 Justify uses the main axis. Along the Direction.

Summary

Describe the layout. Let Retui calculate the space.

The complete mental model in one sentence:

Parents control space. Children describe how they use that space.

The sizing resolution order is always:

Fixed  →  Fit  →  Percent  →  Grow

Remaining space after the first three modes is what Grow children share.


Retui Layout System — github.com/subhasundardass/retui

Clone this wiki locally