Layout System
In Part 2, you learned about the five core elements. Now let’s learn how to position and size them to create professional layouts.
[!NOTE] What is Layout? Layout is the process of determining where each element goes and how big it should be. Freya uses the Torin layout engine, which works similarly to CSS Flexbox. If you know CSS, this will feel familiar!
Understanding the Box Model
Every element in Freya is a rectangular box with these parts:
┌─────────────────────────────────────┐
│ MARGIN │ ← Space outside the element
│ ┌───────────────────────────────┐ │
│ │ BORDER │ │ ← The element's border
│ │ ┌─────────────────────────┐ │ │
│ │ │ PADDING │ │ │ ← Space inside the element
│ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ CONTENT │ │ │ │ ← The actual content
│ │ │ │ │ │ │ │
│ │ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
The total space an element takes up = content + padding + border + margin
Size Units
Freya provides several ways to specify sizes:
Pixels (Fixed Size)
rect()
.width(Size::px(200.0)) // Exactly 200 pixels wide
.height(Size::px(100.0)) // Exactly 100 pixels tall
When to use: When you need a specific, fixed size (icons, avatars, fixed-width elements).
Fill (Take Available Space)
rect()
.width(Size::fill()) // Take all available width
.height(Size::fill()) // Take all available height
When to use: For containers that should expand to fill their parent.
Auto (Size to Content)
rect()
.width(Size::auto()) // Size to fit children
.height(Size::auto())
When to use: When the container should shrink-wrap its content.
Percent (Relative to Parent)
rect()
.width(Size::percent(50.0)) // 50% of parent's width
.height(Size::percent(75.0)) // 75% of parent's height
When to use: For responsive layouts that scale with the parent.
[!WARNING] Percentage Gotcha Percentages are relative to the parent’s size, not the screen. If the parent has
Size::auto(), percentages may not work as expected.
Size Constraints
Control minimum and maximum sizes:
rect()
.width(Size::fill())
.min_width(Size::px(300.0)) // At least 300px
.max_width(Size::px(800.0)) // At most 800px
This creates a flexible element that:
- Expands to fill available space
- Never shrinks below 300px
- Never grows above 800px
Quick Shorthands
// Fill both dimensions
rect()
.expanded() // Same as .width(Size::fill()).height(Size::fill())
// Fill only one dimension
rect()
.fill_width() // Same as .width(Size::fill())
.fill_height() // Same as .height(Size::fill())
// Set both dimensions at once
rect()
.size(200.0, 100.0) // width: 200px, height: 100px
Direction: How Children Are Arranged
Direction determines the main axis - the direction children are stacked.
Vertical Direction (Default)
Children stack top to bottom:
rect()
.direction(Direction::Vertical) // or just don't specify
.child(label().text("First"))
.child(label().text("Second"))
.child(label().text("Third"))
Visual result:
┌─────────────┐
│ First │
├─────────────┤
│ Second │
├─────────────┤
│ Third │
└─────────────┘
Horizontal Direction
Children stack left to right:
rect()
.direction(Direction::Horizontal)
.child(label().text("A"))
.child(label().text("B"))
.child(label().text("C"))
Visual result:
┌───────────────────┐
│ A │ B │ C │
└───────────────────┘
[!TIP] Choosing Direction
- Use Vertical for lists, forms, page sections
- Use Horizontal for toolbars, navigation bars, button groups
Main Axis Alignment
Main axis alignment controls how children are distributed along the main axis.
[!NOTE] Main Axis Definition
- In Vertical direction: main axis = vertical (top to bottom)
- In Horizontal direction: main axis = horizontal (left to right)
Start Alignment (Default)
Children pack at the start:
rect()
.direction(Direction::Vertical)
.main_align_start() // Children start at top
.child(box1())
.child(box2())
┌─────────────┐
│ ┌───┐ │
│ │ 1 │ │
│ └───┘ │
│ ┌───┐ │
│ │ 2 │ │
│ └───┘ │
│ │
│ (empty) │
└─────────────┘
Center Alignment
Children are centered:
rect()
.main_align_center()
┌─────────────┐
│ (empty) │
│ ┌───┐ │
│ │ 1 │ │
│ └───┘ │
│ ┌───┐ │
│ │ 2 │ │
│ └───┘ │
│ (empty) │
└─────────────┘
End Alignment
Children pack at the end:
rect()
.main_align_end()
┌─────────────┐
│ (empty) │
│ │
│ ┌───┐ │
│ │ 1 │ │
│ └───┘ │
│ ┌───┐ │
│ │ 2 │ │
│ └───┘ │
└─────────────┘
Space Between
First child at start, last at end, equal space between:
rect()
.direction(Direction::Horizontal)
.main_align_space_between()
┌─────────────────────┐
│ [A] [B] [C]│
└─────────────────────┘
Perfect for: Header layouts with logo on left, actions on right.
Space Around
Equal space around each child:
rect()
.main_align_space_around()
┌─────────────────────┐
│ [A] [B] [C] │
└─────────────────────┘
Space Evenly
Equal space between and around all children:
rect()
.main_align_space_evenly()
┌─────────────────────┐
│ [A] [B] [C] │
└─────────────────────┘
Cross Axis Alignment
Cross axis alignment controls children on the perpendicular axis.
[!NOTE] Cross Axis Definition
- In Vertical direction: cross axis = horizontal
- In Horizontal direction: cross axis = vertical
Start, Center, End
rect()
.direction(Direction::Vertical)
.cross_align_start() // Children align left
.cross_align_center() // Children centered horizontally
.cross_align_end() // Children align right
Visual for vertical layout:
Start: Center: End:
┌─────────┐ ┌─────────┐ ┌─────────┐
│[A] │ │ [A] │ │ [A]│
│[BB] │ │ [BB] │ │ [BB]│
│[CCC] │ │ [CCC] │ │ [CCC]│
└─────────┘ └─────────┘ └─────────┘
Stretch
Children stretch to fill cross axis:
rect()
.direction(Direction::Vertical)
.cross_align_stretch() // Children fill width
Perfect for full-width buttons in a list:
┌─────────────┐
│ [Button 1] │ ← Stretches to fill width
├─────────────┤
│ [Button 2] │
├─────────────┤
│ [Button 3] │
└─────────────┘
Quick Center Shorthand
To center on both axes:
rect()
.center() // Same as .main_align_center().cross_align_center()
Gap: Spacing Between Children
Gap adds consistent spacing between children:
rect()
.direction(Direction::Vertical)
.gap(16.0) // 16 pixels between each child
.child(item1())
.child(item2())
.child(item3())
Visual:
┌─────────────┐
│ Item 1 │
│ │ ← 16px gap
│ Item 2 │
│ │ ← 16px gap
│ Item 3 │
└─────────────┘
[!TIP] Gap vs Margin Gap is cleaner than adding margin to every child:
// DON'T do this rect() .child(rect().margin_bottom(16.0).child(item1())) .child(rect().margin_bottom(16.0).child(item2())) // DO this instead rect() .gap(16.0) .child(item1()) .child(item2())
Row Gap and Column Gap
For grid-like layouts, you can set different gaps:
rect()
.row_gap(16.0) // Gap between rows
.column_gap(8.0) // Gap between columns
Padding and Margin
Padding (Inside Space)
Padding is space between the element’s border and its content:
rect()
.padding(16.0) // All sides: 16px
.padding_horizontal(24.0) // Left + Right: 24px
.padding_vertical(8.0) // Top + Bottom: 8px
.padding_left(8.0) // Left only
.padding_right(8.0) // Right only
.padding_top(4.0) // Top only
.padding_bottom(4.0) // Bottom only
Visual:
┌─────────────────────────┐
│ ↑ padding_top │
│ ← padding_left │
│ [ CONTENT ] │
│ → │
│ padding_right │
│ ↓ padding_bottom │
└─────────────────────────┘
Margin (Outside Space)
Margin is space outside the element:
rect()
.margin(16.0) // All sides: 16px
.margin_horizontal(24.0) // Left + Right: 24px
.margin_vertical(8.0) // Top + Bottom: 8px
Position: Relative vs Absolute
Relative Position (Default)
Element flows normally with other elements:
rect()
.position(Position::Relative) // Default behavior
Absolute Position
Element is removed from normal flow and positioned relative to nearest positioned ancestor:
rect()
.position(Position::Absolute)
.left(Size::px(10.0)) // 10px from left edge
.top(Size::px(20.0)) // 20px from top edge
[!WARNING] Absolute Position Gotcha Absolute positioned elements don’t affect the size of their parent. The parent might collapse to zero height if all children are absolute.
Use Cases for Absolute Positioning
Overlays and Badges:
rect()
.width(Size::px(50.0))
.height(Size::px(50.0))
.child(icon())
.child(
rect()
.position(Position::Absolute)
.top(Size::px(0.0))
.right(Size::px(0.0))
.width(Size::px(16.0))
.height(Size::px(16.0))
.background(Color::RED)
.corner_radius(8.0)
// This creates a notification badge
)
Modals and Dialogs:
rect()
.expanded()
.child(background_content())
.child(
rect()
.position(Position::Absolute)
.width(Size::fill())
.height(Size::fill())
.background(Color::BLACK.with_a(128)) // Dimmed overlay
.main_align_center()
.cross_align_center()
.child(modal_content())
)
Z-Index: Stacking Order
When elements overlap, z-index controls which appears on top:
rect()
.child(
rect()
.z_index(0) // Bottom layer
)
.child(
rect()
.z_index(1) // Top layer (overlaps the first)
)
.child(
rect()
.z_index(2) // Even higher
)
Overflow: Handling Content That Doesn’t Fit
Clip
Hide content that overflows:
rect()
.width(Size::px(200.0))
.overflow(Overflow::Clip) // Content beyond 200px is hidden
.child(wide_content())
Scroll
Make content scrollable:
rect()
.height(Size::px(300.0))
.overflow(Overflow::Scroll)
.scroll_direction(ScrollDirection::Vertical)
.child(long_list())
Scroll Directions
ScrollDirection::Vertical // Vertical scrolling
ScrollDirection::Horizontal // Horizontal scrolling
ScrollDirection::Both // Both directions
Controlling Scroll Programmatically
fn scrollable_list() -> impl IntoElement {
let scroll = use_scroll();
rect()
.height(Size::px(300.0))
.overflow(Overflow::Scroll)
.scroll_handle(scroll)
.child(long_content())
.child(
Button::new()
.on_press(move |_| {
scroll.scroll_to(ScrollPosition::Top);
})
.child("Scroll to Top")
)
}
Scroll positions:
ScrollPosition::Top- Scroll to topScrollPosition::Bottom- Scroll to bottomScrollPosition::Offset(100.0)- Scroll to specific position
Layout Examples
Example 1: Two-Column Layout
A classic sidebar + main content layout:
fn two_column_layout() -> impl IntoElement {
rect()
.expanded()
.direction(Direction::Horizontal)
.child(
// Sidebar - fixed width
rect()
.width(Size::px(250.0))
.height(Size::fill())
.background(Color::from_rgb(30, 30, 30))
.padding(16.0)
.direction(Direction::Vertical)
.gap(8.0)
.child(nav_item("Home"))
.child(nav_item("Settings"))
.child(nav_item("Profile"))
)
.child(
// Main content - fills remaining space
rect()
.width(Size::fill())
.height(Size::fill())
.padding(24.0)
.child(main_content())
)
}
Example 2: Header with Actions
Space-between pattern for headers:
fn header() -> impl IntoElement {
rect()
.width(Size::fill())
.height(Size::px(60.0))
.padding_horizontal(24.0)
.direction(Direction::Horizontal)
.main_align_space_between() // Push logo and actions to edges
.cross_align_center() // Vertically center
.background(Color::WHITE)
.border(Border::new().width(1.0).fill(Color::LIGHT_GRAY))
.child(
label()
.text("MyApp")
.font_size(20.0)
.font_weight(FontWeight::Bold)
)
.child(
rect()
.direction(Direction::Horizontal)
.gap(8.0)
.child(Button::new().child("Sign In"))
.child(Button::new().child("Sign Up"))
)
}
Example 3: Centered Card
A card perfectly centered in the viewport:
fn centered_card() -> impl IntoElement {
rect()
.expanded()
.background(Color::from_rgb(240, 240, 240))
.main_align_center()
.cross_align_center()
.child(
rect()
.width(Size::px(400.0))
.background(Color::WHITE)
.corner_radius(12.0)
.padding(24.0)
.shadow(
Shadow::new()
.color(Color::BLACK.with_a(50))
.y(4.0)
.blur(20.0)
)
.direction(Direction::Vertical)
.gap(16.0)
.child(
label()
.text("Welcome!")
.font_size(24.0)
.font_weight(FontWeight::Bold)
)
.child(
label()
.text("Sign in to continue")
.color(Color::GRAY)
)
.child(login_form())
)
}
Example 4: Responsive Grid
A grid that adapts to content:
fn item_grid() -> impl IntoElement {
rect()
.width(Size::fill())
.direction(Direction::Horizontal)
.gap(16.0)
.child(grid_item(1))
.child(grid_item(2))
.child(grid_item(3))
}
fn grid_item(id: i32) -> impl IntoElement {
rect()
.width(Size::fill()) // Each item shares available space
.height(Size::px(200.0))
.background(Color::WHITE)
.corner_radius(8.0)
.main_align_center()
.cross_align_center()
.child(label().text(format!("Item {}", id)))
}
Example 5: Footer with Stacked Content
A footer with logo and links:
fn footer() -> impl IntoElement {
rect()
.width(Size::fill())
.padding(32.0)
.background(Color::from_rgb(30, 30, 30))
.direction(Direction::Vertical)
.gap(24.0)
.cross_align_center()
.child(
label()
.text("MyApp")
.font_size(24.0)
.font_weight(FontWeight::Bold)
.color(Color::WHITE)
)
.child(
rect()
.direction(Direction::Horizontal)
.gap(24.0)
.child(link("Privacy"))
.child(link("Terms"))
.child(link("Contact"))
)
.child(
label()
.text("© 2026 MyApp. All rights reserved.")
.font_size(12.0)
.color(Color::GRAY)
)
}
Common Layout Mistakes
Mistake 1: Missing Height in Scrollable Container
Problem: Scroll doesn’t work.
rect()
.overflow(Overflow::Scroll)
// Missing height! Container collapses to content size
.child(long_content())
Solution: Always set a fixed height for scrollable containers:
rect()
.height(Size::px(400.0)) // Fixed height
.overflow(Overflow::Scroll)
.child(long_content())
Mistake 2: Direction Confusion
Problem: Elements don’t align as expected.
rect()
// Forgot direction, defaults to vertical
.cross_align_center() // Expecting horizontal center but it centers vertically
.child(content())
Solution: Always be explicit about direction when alignment matters.
Mistake 3: Fill in Auto Container
Problem: Element with Size::fill() has zero size.
rect()
.width(Size::auto()) // Parent has auto width
.child(
rect()
.width(Size::fill()) // Fill what? Parent has no size!
)
Solution: Ensure parents have defined sizes when children use Size::fill().
Layout Debugging Tips
Visualizing Layouts with Background Colors
Temporarily add distinct background colors to see element boundaries:
rect()
.background(Color::RED) // Outer container
.child(
rect()
.background(Color::BLUE) // Inner container
.child(content())
)
Using Borders for Debugging
rect()
.border(Border::new().width(1.0).fill(Color::RED))
This helps you see exactly where each element’s box is.
Summary
In this tutorial, you learned:
- Size units:
px,fill,auto,percent, and constraints - Direction: How children are arranged (vertical/horizontal)
- Main axis alignment:
start,center,end,space_between,space_around,space_evenly - Cross axis alignment:
start,center,end,stretch - Gap: Spacing between children
- Padding and margin: Inside and outside spacing
- Position: Relative vs absolute positioning
- Overflow: Handling content that doesn’t fit
- Layout patterns: Two-column, header, centered card, grid, footer
Previous: Part 2: Core Elements ←
Next: Part 4: Styling →
In the next tutorial, we’ll explore styling in depth - colors, borders, shadows, gradients, and typography to make your UIs look polished and professional.