Part 3 of 14 17 Jan 2026 By Raj Patil 35 min read

Part 3: Layout System - Positioning and Sizing Elements

Part 3 of the Freya Rust GUI series. Master the layout system with size units, padding, margin, direction, alignment, gaps, and positioning to create responsive, professional interfaces.

Beginner #rust #freya #gui #layout #flexbox #responsive #tutorial
Building Native GUIs with Rust & Freya 3 / 14

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:

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:


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)))
}

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:


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.