Part 2 of 14 16 Jan 2026 By Raj Patil 35 min read

Part 2: Core Elements - Building Blocks of Freya UIs

Part 2 of the Freya Rust GUI series. Master the five core elements - rect, label, paragraph, svg, and image - with detailed examples and practical patterns for building any interface.

Beginner #rust #freya #gui #elements #rect #label #svg #image #tutorial
Building Native GUIs with Rust & Freya 2 / 14

Core Elements

In Part 1, you built your first Freya application using rect() and label(). In this tutorial, we’ll explore all five core elements in depth - the fundamental building blocks that every Freya UI is made of.

[!NOTE] Think Like Lego Every complex interface you’ve ever seen is built from simple elements. A Facebook feed? Rectangles with images and labels. A Spotify player? Rectangles with images, labels, and SVG icons. Master these basics, and you can build anything.

The Five Core Elements

Freya provides five fundamental elements:

ElementPurposeHTML Equivalent
rect()Container for other elements<div>
label()Simple text display<span>
paragraph()Advanced text with formatting<p> with <span>
svg()Vector graphics and icons<svg>
image()Display images<img>

Everything you see in a Freya app is built by combining these five elements. Let’s explore each one.


rect() - The Container Element

rect() is the most important element. It’s a rectangular container that holds other elements, like a box.

Why Do We Need Containers?

Think of a webpage layout:

Rectangles within rectangles - that’s how GUIs work!

Basic rect() Usage

rect()
    .width(Size::px(200.0))       // 200 pixels wide
    .height(Size::px(100.0))      // 100 pixels tall
    .background(Color::BLUE)      // Blue background
    .child(label().text("Hi!"))   // Content inside

This creates a 200x100 blue rectangle with text inside.

Sizing Options

Freya provides several ways to specify sizes:

// Fixed size in pixels
rect()
    .width(Size::px(200.0))
    .height(Size::px(100.0))

// Fill available space
rect()
    .width(Size::fill())    // "Take all width available"
    .height(Size::fill())   // "Take all height available"

// Auto size based on content
rect()
    .width(Size::auto())    // "Size to fit my children"
    .height(Size::auto())

// Percentage of parent
rect()
    .width(Size::percent(50.0))   // 50% of parent's width
    .height(Size::percent(25.0))  // 25% of parent's height

[!TIP] Quick Shorthands Instead of .width(Size::fill()).height(Size::fill()), you can use:

rect()
    .expanded()  // Same as width: fill, height: fill

Size Constraints

Control minimum and maximum sizes:

rect()
    .width(Size::fill())
    .min_width(Size::px(200.0))   // At least 200px wide
    .max_width(Size::px(600.0))   // At most 600px wide
    .height(Size::auto())
    .min_height(Size::px(50.0))   // At least 50px tall

This is useful for responsive layouts that need to work at different window sizes.

Padding and Margin

Padding is space inside the element (between border and content):

rect()
    .padding(16.0)              // 16px on all sides
    .padding_horizontal(24.0)   // Left and right: 24px
    .padding_vertical(8.0)      // Top and bottom: 8px

Margin is space outside the element (between this element and siblings):

rect()
    .margin(16.0)               // 16px on all sides
    .margin_horizontal(24.0)    // Left and right: 24px
    .margin_vertical(8.0)       // Top and bottom: 8px

[!NOTE] Visualizing Padding vs Margin

┌─────────────────────────┐ ← Element border
│        MARGIN           │ ← Space outside
│  ┌─────────────────┐    │
│  │    PADDING      │    │ ← Space inside
│  │  ┌───────────┐  │    │
│  │  │  CONTENT  │  │    │
│  │  └───────────┘  │    │
│  └─────────────────┘    │
└─────────────────────────┘

Rounded Corners

rect()
    .corner_radius(8.0)                    // All corners: 8px radius
    .corner_radius(16.0, 4.0, 16.0, 4.0)   // TL, TR, BR, BL individually

Larger values = more rounded. A corner_radius of half the height creates a “pill” shape:

rect()
    .height(Size::px(40.0))
    .corner_radius(20.0)   // Perfect pill/button shape

Background Colors

rect()
    .background(Color::WHITE)                          // Named color
    .background(Color::from_rgb(255, 0, 0))            // RGB
    .background(Color::from_hex("#ff6600").unwrap())   // Hex

Borders

rect()
    .border(
        Border::new()
            .width(2.0)              // 2 pixels thick
            .fill(Color::BLACK)      // Black color
    )

Shadows

Shadows add depth and make elements “pop”:

rect()
    .shadow(
        Shadow::new()
            .color(Color::BLACK)     // Shadow color
            .x(0.0)                  // Horizontal offset
            .y(4.0)                  // Vertical offset (down)
            .blur(10.0)              // How blurry
    )

[!TIP] Realistic Shadow Recipe For a natural-looking shadow, use:

  • Dark color with transparency: Color::BLACK.with_a(50)
  • Small Y offset (2-6 pixels)
  • Blur radius of 8-20 pixels

Visual Effects

Apply visual transformations and effects to your rectangles:

// Opacity (0.0 = invisible, 1.0 = fully visible)
rect()
    .opacity(0.5)      // 50% transparent

// Rotation (degrees)
rect()
    .rotate(45.0)      // Rotate 45 degrees

// Scale (enlarge or shrink)
rect()
    .scale(Scale::new(1.5))   // 150% size

// Blur effect
rect()
    .blur(5.0)         // Apply blur filter

// Overflow handling
rect()
    .overflow(Overflow::Clip)    // Clip content to bounds
    .overflow(Overflow::Scroll)  // Make scrollable

Position Control

Control how elements are positioned:

// Relative positioning (default) - flows with layout
rect()
    .position(Position::Relative)

// Absolute positioning - placed at exact coordinates
rect()
    .position(Position::Absolute)
    .left(Size::px(10.0))    // 10px from left
    .top(Size::px(20.0))     // 20px from top

// Layer ordering (higher values appear on top)
rect()
    .z_index(1)              // Above z_index(0)

[!NOTE] When to Use Absolute Positioning Use Position::Absolute sparingly - for overlays, tooltips, modals, or floating elements. Most layouts should use relative positioning with flexbox.

Alignment Shorthand Methods

Freya provides convenient shorthand methods for common alignment patterns:

// Center content both horizontally and vertically
rect()
    .center()          // Same as main_align_center() + cross_align_center()

// Center on one axis only
rect()
    .center_x()        // Center horizontally
    .center_y()        // Center vertically

// Fill available space
rect()
    .fill_width()      // width: Size::fill()
    .fill_height()     // height: Size::fill()

// Fill everything
rect()
    .expanded()        // width + height: fill

Complete rect() Example: A Card

fn card() -> impl IntoElement {
    rect()
        .width(Size::px(300.0))
        .background(Color::WHITE)
        .corner_radius(12.0)
        .padding(16.0)
        .border(Border::new().width(1.0).fill(Color::LIGHT_GRAY))
        .shadow(
            Shadow::new()
                .color(Color::BLACK.with_a(30))
                .y(4.0)
                .blur(12.0)
        )
        .child(
            label()
                .text("Card Title")
                .font_size(20.0)
                .font_weight(FontWeight::Bold)
        )
}

label() - Simple Text

label() displays a single line of text. It’s the simplest way to show text.

Basic Usage

label()
    .text("Hello, World!")

Text Styling

label()
    .text("Styled Text")
    .font_size(24.0)                        // Size in points
    .color(Color::RED)                      // Text color
    .font_weight(FontWeight::Bold)          // Bold text
    .font_family("Inter")                   // Font name
    .line_height(1.5)                       // Spacing between lines

Font Weights

FontWeight::Thin        // 100 - Very thin
FontWeight::Light       // 300
FontWeight::Normal      // 400 - Default
FontWeight::Medium      // 500
FontWeight::SemiBold    // 600
FontWeight::Bold        // 700 - Bold
FontWeight::ExtraBold   // 800
FontWeight::Black       // 900 - Very thick

Text Alignment

label()
    .text("Aligned Text")
    .text_align(TextAlign::Start)    // Left (in LTR languages)
    .text_align(TextAlign::Center)   // Centered
    .text_align(TextAlign::End)      // Right (in LTR languages)

Limiting Lines

label()
    .text("Very long text that might not fit...")
    .max_lines(1)    // Only show one line

When to Use label() vs paragraph()

Use label() WhenUse paragraph() When
Single style of textMixed styles (bold + normal)
Short, simple textMultiple spans needed
UI elements like buttonsRich text formatting

paragraph() - Rich Text

paragraph() is for advanced text with mixed formatting. Think of it like writing in a word processor where different words can have different styles.

The Problem with label()

What if you want “Hello, World!” where “World” is bold but “Hello, ” isn’t? You can’t do that with label().

The Solution: paragraph() with Spans

paragraph()
    .child(Span::new("Hello, ").color(Color::BLACK))
    .child(Span::new("World!").color(Color::RED).font_weight(FontWeight::Bold))

Each Span is a piece of text with its own styling.

Span Styling Options

Span::new("styled text")
    .color(Color::RED)                    // Text color
    .font_size(20.0)                      // Size
    .font_weight(FontWeight::Bold)        // Weight
    .font_style(FontStyle::Italic)        // Italic
    .font_family("Arial")                 // Font

Complete paragraph() Example

fn formatted_text() -> impl IntoElement {
    paragraph()
        .font_size(16.0)
        .line_height(1.6)
        .child(Span::new("This is ").color(Color::BLACK))
        .child(
            Span::new("bold text")
                .font_weight(FontWeight::Bold)
                .color(Color::BLACK)
        )
        .child(Span::new(" and this is ").color(Color::BLACK))
        .child(
            Span::new("italic")
                .font_style(FontStyle::Italic)
                .color(Color::GRAY)
        )
        .child(Span::new(".").color(Color::BLACK))
}

Paragraph-Level Styling

Some styles apply to the whole paragraph:

paragraph()
    .font_size(16.0)              // Default size for all spans
    .color(Color::BLACK)          // Default color
    .line_height(1.5)             // Line spacing
    .max_lines(3)                 // Maximum lines (ellipsis for overflow)
    .text_align(TextAlign::Center) // Alignment
fn link(text: &str) -> impl IntoElement {
    paragraph()
        .child(
            Span::new(text)
                .color(Color::BLUE)
                .font_weight(FontWeight::Medium)
        )
    // Note: You'd add .on_pointer_down() for click handling
}

svg() - Vector Graphics

svg() displays SVG (Scalable Vector Graphics) - perfect for icons and illustrations.

What is SVG?

SVG is a format for graphics defined as code (XML), not pixels. Benefits:

Basic svg() Usage

// SVG content as a string
let svg_content = r#"
<svg viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2L2 7l10 5 10-5-10-5z"/>
</svg>
"#;

svg()
    .svg_data(svg_content)
    .width(Size::px(24.0))
    .height(Size::px(24.0))
    .color(Color::BLUE)   // Tint color

Loading SVG from File

svg()
    .svg_resource("icons/logo.svg")   // Path relative to project
    .width(Size::px(48.0))
    .height(Size::px(48.0))

[!TIP] Where to Get SVG Icons

Common Icon Example

Here’s a simple “plus” icon:

fn plus_icon() -> impl IntoElement {
    let svg_data = r#"
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <line x1="12" y1="5" x2="12" y2="19"></line>
        <line x1="5" y1="12" x2="19" y2="12"></line>
    </svg>
    "#;
    
    svg()
        .svg_data(svg_data)
        .width(Size::px(24.0))
        .height(Size::px(24.0))
        .color(Color::BLACK)
}

Using SVG in Buttons

fn icon_button() -> impl IntoElement {
    let icon_svg = r#"<svg viewBox="0 0 24 24" fill="currentColor">
        <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
    </svg>"#;
    
    rect()
        .width(Size::px(40.0))
        .height(Size::px(40.0))
        .background(Color::BLUE)
        .corner_radius(8.0)
        .main_align_center()
        .cross_align_center()
        .child(
            svg()
                .svg_data(icon_svg)
                .width(Size::px(24.0))
                .height(Size::px(24.0))
                .color(Color::WHITE)
        )
}

image() - Displaying Images

image() displays raster images (PNG, JPEG, etc.).

Basic Usage with Bytes

// Load image bytes (from include_bytes! at compile time)
let image_bytes = include_bytes!("../../assets/photo.png");

image()
    .image_data(image_bytes)
    .width(Size::px(200.0))
    .height(Size::px(150.0))

Loading from File Path

image()
    .image_resource("assets/photo.png")
    .width(Size::px(200.0))
    .height(Size::px(150.0))

Image Fitting Options

Control how images fill their container:

image()
    .image_data(image_bytes)
    .width(Size::px(200.0))
    .height(Size::px(200.0))
    .image_fill(ImageFill::Cover)   // Cover the area, crop if needed

Common options:

Practical Example: Avatar

fn avatar(image_bytes: &'static [u8]) -> impl IntoElement {
    rect()
        .width(Size::px(48.0))
        .height(Size::px(48.0))
        .corner_radius(24.0)          // Circular
        .overflow(Overflow::Clip)     // Clip to corners
        .child(
            image()
                .image_data(image_bytes)
                .width(Size::fill())
                .height(Size::fill())
                .image_fill(ImageFill::Cover)
        )
}

Combining Elements - Real World Examples

Now let’s combine all five elements to create real UI components.

Example 1: User Card

A card showing a user’s profile:

fn user_card() -> impl IntoElement {
    let avatar_svg = r#"<svg viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
    </svg>"#;
    
    rect()
        .width(Size::px(280.0))
        .background(Color::WHITE)
        .corner_radius(12.0)
        .padding(16.0)
        .direction(Direction::Horizontal)
        .gap(12.0)
        .child(
            // Avatar
            rect()
                .width(Size::px(56.0))
                .height(Size::px(56.0))
                .corner_radius(28.0)
                .background(Color::LIGHT_GRAY)
                .main_align_center()
                .cross_align_center()
                .child(
                    svg()
                        .svg_data(avatar_svg)
                        .width(Size::px(32.0))
                        .height(Size::px(32.0))
                        .color(Color::GRAY)
                )
        )
        .child(
            // User info
            rect()
                .width(Size::fill())
                .direction(Direction::Vertical)
                .gap(4.0)
                .child(
                    label()
                        .text("Jane Doe")
                        .font_size(16.0)
                        .font_weight(FontWeight::Bold)
                )
                .child(
                    label()
                        .text("Software Engineer")
                        .font_size(14.0)
                        .color(Color::GRAY)
                )
                .child(
                    label()
                        .text("[email protected]")
                        .font_size(12.0)
                        .color(Color::LIGHT_GRAY)
                )
        )
}

Example 2: Notification Item

A notification with an icon, title, and timestamp:

fn notification() -> impl IntoElement {
    let bell_svg = r#"<svg viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>
    </svg>"#;
    
    rect()
        .width(Size::px(350.0))
        .background(Color::WHITE)
        .padding(12.0)
        .direction(Direction::Horizontal)
        .gap(12.0)
        .border(Border::new().width(1.0).fill(Color::LIGHT_GRAY))
        .corner_radius(8.0)
        .child(
            // Icon
            rect()
                .width(Size::px(40.0))
                .height(Size::px(40.0))
                .background(Color::from_rgb(230, 245, 255))
                .corner_radius(20.0)
                .main_align_center()
                .cross_align_center()
                .child(
                    svg()
                        .svg_data(bell_svg)
                        .width(Size::px(20.0))
                        .height(Size::px(20.0))
                        .color(Color::from_rgb(0, 120, 200))
                )
        )
        .child(
            // Content
            rect()
                .width(Size::fill())
                .direction(Direction::Vertical)
                .gap(4.0)
                .child(
                    paragraph()
                        .child(Span::new("New message from ").color(Color::BLACK))
                        .child(Span::new("John").font_weight(FontWeight::Bold).color(Color::BLACK))
                )
                .child(
                    label()
                        .text("2 minutes ago")
                        .font_size(12.0)
                        .color(Color::GRAY)
                )
        )
}

Example 3: Product Card

An e-commerce style product card:

fn product_card() -> impl IntoElement {
    // Placeholder image (a colored rectangle in this example)
    let star_svg = r#"<svg viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
    </svg>"#;
    
    rect()
        .width(Size::px(220.0))
        .background(Color::WHITE)
        .corner_radius(12.0)
        .overflow(Overflow::Clip)
        .shadow(
            Shadow::new()
                .color(Color::BLACK.with_a(40))
                .y(2.0)
                .blur(8.0)
        )
        .child(
            // Product image placeholder
            rect()
                .height(Size::px(160.0))
                .width(Size::fill())
                .background(Color::from_rgb(240, 240, 240))
                .main_align_center()
                .cross_align_center()
                .child(
                    label()
                        .text("Product Image")
                        .color(Color::GRAY)
                )
        )
        .child(
            // Product info
            rect()
                .padding(12.0)
                .direction(Direction::Vertical)
                .gap(8.0)
                .child(
                    label()
                        .text("Wireless Headphones")
                        .font_size(16.0)
                        .font_weight(FontWeight::Bold)
                )
                .child(
                    // Rating
                    rect()
                        .direction(Direction::Horizontal)
                        .gap(4.0)
                        .child(
                            svg()
                                .svg_data(star_svg)
                                .width(Size::px(16.0))
                                .height(Size::px(16.0))
                                .color(Color::from_rgb(255, 193, 7))
                        )
                        .child(
                            label()
                                .text("4.8 (120 reviews)")
                                .font_size(12.0)
                                .color(Color::GRAY)
                        )
                )
                .child(
                    label()
                        .text("$79.99")
                        .font_size(20.0)
                        .font_weight(FontWeight::Bold)
                        .color(Color::from_rgb(0, 120, 50))
                )
        )
}

Element Composition Patterns

Pattern 1: Stacking (Vertical)

Stack elements top to bottom:

rect()
    .direction(Direction::Vertical)
    .gap(16.0)
    .child(element_1())
    .child(element_2())
    .child(element_3())

Pattern 2: Row (Horizontal)

Arrange elements left to right:

rect()
    .direction(Direction::Horizontal)
    .gap(8.0)
    .child(element_a())
    .child(element_b())
    .child(element_c())

Pattern 3: Card Pattern

Outer container + content:

rect()
    .background(...)
    .corner_radius(...)
    .padding(...)
    .shadow(...)
    .child(content())

Pattern 4: Overlay Pattern

Stack elements on top of each other:

rect()
    .width(Size::px(200.0))
    .height(Size::px(100.0))
    .child(background_element())
    .child(
        rect()
            .position(Position::Absolute)
            .width(Size::fill())
            .height(Size::fill())
            .child(overlay_content())
    )

Element Conversion

Freya provides a convenient shortcut: strings automatically convert to labels.

The Shortcut

// These are equivalent:
rect().child(label().text("Hello"))
rect().child("Hello")                    // String converts to label!

// Works with format strings too:
rect().child(format!("Count: {}", 5))

This makes your code cleaner and more readable:

// Before (verbose)
rect()
    .child(label().text("Name:"))
    .child(label().text(name))

// After (concise)
rect()
    .child("Name:")
    .child(name)

[!TIP] When to Use Explicit Labels Use the string shortcut for simple text. Use explicit label() when you need styling:

rect()
    .child("Plain text")                          // Shortcut
    .child(label().text("Bold").font_weight(FontWeight::Bold))  // Explicit

Common Mistakes

Forgetting Direction

Problem: Elements overlap unexpectedly.

rect()
    // Missing .direction() - defaults to vertical, but you wanted horizontal?
    .child(label().text("First"))
    .child(label().text("Second"))

Solution: Always be explicit about direction:

rect()
    .direction(Direction::Horizontal)
    .child(label().text("First"))
    .child(label().text("Second"))

Wrong Size Values

Problem: Element disappears or is too small.

rect()
    .width(Size::auto())      // Might be 0 if no content!
    .height(Size::auto())

Solution: Use fixed or fill sizes when appropriate:

rect()
    .width(Size::px(200.0))   // Guaranteed size
    .height(Size::px(100.0))

Text Truncation

Problem: Text gets cut off.

label()
    .text("Very long text here...")
    .width(Size::px(50.0))   // Too narrow!

Solution: Either:

  1. Make the container wider
  2. Use paragraph() with max_lines() and ellipsis

Summary

In this tutorial, you learned:


Previous: Part 1: Getting Started ←

Next: Part 3: Layout System →

In the next tutorial, we’ll dive deep into the layout system - understanding how to position elements, control spacing, and create responsive designs that work at any window size.