-
Notifications
You must be signed in to change notification settings - Fork 4
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.
- 1. The Core Mental Model
- 2. Parent → Child Layout
- 3. Sizing Modes
- 4. Spacing
- 5. Direction
- 6. Alignment
- 7. Justify
- 8. Overflow and Scrolling
- 9. Wrap
- 10. Spacer
- 11. The Layout Pipeline
- 12. A Complete Example
- 13. Component Reusability
- 14. Decision Checklist
- 15. Quick Reference
- 16. Layout Rules
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.
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.
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 |
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
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
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
└────────────────────────────────────┘
Growis 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
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 ────────────────────────────
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,Percentdepends on the parent's resolved size being known before children are laid out. For most casesGrowis simpler and more predictable.
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
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
│ └─────────────────┘ │
│ │
└───────────────────────────────┘
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
Gapfor spacing between siblings. Avoid inserting emptyBoxelements manually.
Retui supports two layout directions:
Children are arranged horizontally (left to right).
Main axis → X (horizontal)
Cross axis → Y (vertical)
┌────────┬────────┬────────┐
│ Child1 │ Child2 │ Child3 │
└────────┴────────┴────────┘
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
AlignandJustify.
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
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ ████ │ │ ████ │ │ ████ │
│ ████ │ │ ████ │ │ ████ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
Aligncontrols the cross axis. ForColumn, that is horizontal. ForRow, that is vertical.
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 │
└──────────────┘
Justifycontrols the main axis. ForColumn, that is vertical. ForRow, that is horizontal.
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.
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.
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.
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
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.
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)When laying out a component, answer these questions in order:
Sizing
- Should the element have an exact size? →
Fixed(n) - Should the element size itself from content? →
Fit() - Should the element fill remaining space? →
Grow(n) - Should the element be proportional to its parent? →
Percent(n)
Direction and spacing
- Should children be horizontal or vertical? →
Direction: Row/Column - How much space between siblings? →
Gap - How much inner breathing room? →
Padding - How much outer breathing room? →
Margin
Alignment
- How should children align on the cross axis? →
Align - How should children distribute on the main axis? →
Justify
Edge cases
- What happens if content is too tall/wide? →
Overflow - Should rows wrap when they run out of width? →
Wrap
| 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 |
| # | 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. |
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