Part 1 of 14 15 Jan 2026 By Raj Patil 30 min read

Part 1: Getting Started with Freya - Building Native Rust GUIs

Part 1 of the Freya Rust GUI series. A complete beginner's guide to setting up your environment, understanding GUI concepts, and building your first high-performance native desktop app with Rust.

Beginner #rust #freya #gui #desktop-app #rust-ui #native-ui #tutorial #beginner
Building Native GUIs with Rust & Freya 1 / 14

Getting Started with Freya

Welcome to the first tutorial in the Building Native GUIs with Rust & Freya series! If you’ve ever wanted to build beautiful, high-performance desktop applications using Rust, you’re in the right place.

[!NOTE] New to GUI Programming? Don’t worry! This tutorial assumes no prior GUI experience. We’ll explain everything from the ground up, including what GUI even means and how graphical applications work.

What is a GUI?

Before we dive into Freya, let’s understand what we’re building.

GUI stands for Graphical User Interface (pronounced “gooey”). It’s the visual part of an application that users interact with - windows, buttons, text boxes, menus, and all the visual elements you see and click on.

GUI vs CLI

TypeWhat It IsExample
CLI (Command Line Interface)Text-based interaction in a terminalcargo run, git commit
GUI (Graphical User Interface)Visual windows with clickable elementsVS Code, Firefox, Spotify

When you use cargo run in your terminal, you’re using a CLI. When you click a button in your web browser, you’re using a GUI. Freya helps you build GUIs with Rust.

How Do GUIs Work?

At a fundamental level, every GUI application follows this pattern:

  1. Create a Window - The operating system gives you a rectangular area to draw in
  2. Draw UI Elements - You tell the computer what to display (buttons, text, images)
  3. Handle Events - When users click, type, or scroll, your app responds
  4. Update Display - When data changes, redraw the screen to reflect it
  5. Repeat - This cycle continues until the app closes

This cycle happens 60 times per second (or more!) in most GUI applications, creating the illusion of smooth, responsive interfaces.

What is Freya?

Freya is a cross-platform GUI framework for Rust. Let’s break that down:

The Technology Stack

Freya is built on two powerful technologies:

  1. Dioxus - A Rust framework for building user interfaces (similar to React in JavaScript)
  2. Skia - Google’s graphics library used by Chrome, Android, and Flutter

[!TIP] Why Skia Matters Skia is the same graphics engine used by Chrome browser and Android. It uses your computer’s graphics card (GPU) to render smooth, fast visuals. This means your Freya apps will look great and perform well even with complex animations.

Why Choose Freya?

FeatureWhat It Means For You
GPU AcceleratedYour apps run smoothly without lag
Reactive StateWhen data changes, the UI updates automatically
Declarative UIYou describe what the UI should look like, not how to draw it
Cross-PlatformWrite once, run on Windows/Mac/Linux
Built-in ComponentsButtons, inputs, sliders - all ready to use
Theming SupportEasy light/dark mode and custom colors
Animation SystemSmooth transitions and effects
AccessibilityWorks with screen readers and keyboard navigation

Freya vs Other Options

FrameworkLanguageRenderingBest For
FreyaRustSkia (GPU)Native desktop apps with Rust
TauriRust + WebWebViewApps with web technologies
ElectronJavaScriptChromiumWeb developers
FlutterDartSkiaMobile + Desktop
QtC++NativeEnterprise applications

If you want to build desktop apps with pure Rust and get native performance, Freya is an excellent choice.

Prerequisites

Before we begin, make sure you have the following:

1. Rust Installed

You need Rust installed on your computer. Open your terminal and run:

rustc --version

You should see something like rustc 1.75.0. If you get an error, install Rust:

# Install Rust using rustup (the recommended way)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, restart your terminal and verify:

rustc --version
cargo --version

[!TIP] What’s the difference between rustc and cargo?

  • rustc is the Rust compiler - it turns your code into executable programs
  • cargo is Rust’s package manager - it manages dependencies, runs tests, and builds projects
  • You’ll mostly use cargo day-to-day; it calls rustc automatically

2. A Code Editor

VS Code with the rust-analyzer extension is highly recommended:

  1. Install VS Code
  2. Open Extensions (Ctrl+Shift+X)
  3. Search for “rust-analyzer” and install it
  4. Search for “CodeLLDB” and install it (for debugging)

3. Basic Rust Knowledge

You should be familiar with:

[!NOTE] Rust Basics Refresher If you’re new to Rust, consider going through The Rust Book chapters 1-10 first. You don’t need to be an expert - just understand the basics of ownership, borrowing, and structs.

Platform-Specific Dependencies

Linux:

Linux requires some system libraries for GUI development:

# Debian/Ubuntu/Pop!_OS
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev

# Fedora
sudo dnf install webkit2gtk4.1-devel gtk3-devel libappindicator-gtk3-devel librsvg2-devel

# Arch/Manjaro
sudo pacman -S webkit2gtk-4.1 gtk3 libayatana-appindicator librsvg

Windows & macOS: No additional dependencies required! Rust and Cargo handle everything.

Setting Up Your Project

Let’s create a new Freya project step by step.

Step 1: Create a New Cargo Project

Open your terminal and run:

cargo new my-freya-app
cd my-freya-app

This creates a new directory with this structure:

my-freya-app/
├── Cargo.toml      # Project configuration and dependencies
└── src/
    └── main.rs     # Your application code

Step 2: Understand Cargo.toml

Open Cargo.toml in your editor. You’ll see:

[package]
name = "my-freya-app"
version = "0.1.0"
edition = "2021"

[dependencies]

The [dependencies] section is where we list external libraries (called “crates” in Rust) that our project needs.

Step 3: Add Freya Dependency

Add Freya to your dependencies:

[package]
name = "my-freya-app"
version = "0.1.0"
edition = "2021"

[dependencies]
freya = "0.4"

[!NOTE] Version Numbers The "0.4" is a version number. Freya is evolving rapidly, so check crates.io/crates/freya for the latest version. If you use an older version, some features might work differently.

Step 4: Feature Flags (Optional)

Freya has optional features you can enable:

[dependencies.freya]
version = "0.4"
features = [
    "tray",       # System tray icon support
    "devtools",   # Developer tools for debugging
    "hot-reload", # Automatically refresh UI when code changes
]

[!TIP] For Development: Enable hot-reload during development - it saves you from manually restarting your app every time you make changes.

Step 5: Verify Installation

Run this command to download and compile Freya:

cargo build

The first build will take a few minutes because it’s compiling Freya and all its dependencies. This is normal! Subsequent builds will be much faster.

Your First Freya Application

Now for the exciting part - let’s build our first GUI application!

The Complete Code

Replace the contents of src/main.rs with this:

// Import all the commonly used Freya types and functions
use freya::prelude::*;

// This is where your program starts
fn main() {
    // launch() starts the GUI application
    // We pass it our main component (the app function)
    launch(app);
}

// This is a component - it describes what should appear on screen
// -> impl IntoElement means "returns something that can be displayed"
fn app() -> impl IntoElement {
    // rect() creates a rectangle container (like a <div> in HTML)
    rect()
        // Make the rectangle fill the entire window
        .expanded()
        // Set the background color (dark gray)
        .background(Color::from_rgb(30, 30, 30))
        // Center content vertically
        .main_align_center()
        // Center content horizontally
        .cross_align_center()
        // Add a child element (the text)
        .child(
            // label() displays text
            label()
                // Set the font size
                .font_size(32.0)
                // Set the text color (white)
                .color(Color::WHITE)
                // Set the text content
                .text("Hello, Freya!")
        )
}

Running Your App

Save the file and run:

cargo run

You should see a window appear with “Hello, Freya!” centered on a dark background. Congratulations - you’ve built your first GUI application!

Understanding Every Line

Let’s go through each part in detail:

The Import Statement

use freya::prelude::*;

This single line gives you access to launch, rect, label, Color, and many other useful types.

The main Function

fn main() {
    launch(app);
}

[!NOTE] What’s an Event Loop? GUI applications need to continuously:

  1. Check for user input (mouse clicks, key presses)
  2. Update the display
  3. Handle system events

This is called an “event loop,” and launch() sets it up for you automatically.

The Component Function

fn app() -> impl IntoElement {

This is called a function component. It’s a simple way to define UI without creating a struct.

The rect Element

rect()
    .expanded()
    .background(Color::from_rgb(30, 30, 30))

[!TIP] Method Chaining This pattern of calling methods one after another (.method1().method2().method3()) is called “method chaining” or “builder pattern.” Each method returns the same object, modified, so you can keep adding more modifications.

Colors

Color::from_rgb(30, 30, 30)  // Dark gray
Color::WHITE                  // Pure white

Colors in Freya are created with:

RGB values of (30, 30, 30) give us a dark gray because all three colors are equal and low.

Alignment

.main_align_center()
.cross_align_center()

These control how children are positioned within the rect:

Both set to “center” means the child will be perfectly centered.

Adding Children

.child(
    label()
        .font_size(32.0)
        .color(Color::WHITE)
        .text("Hello, Freya!")
)

A More Interactive Example

Let’s make something more interesting - a counter with buttons!

use freya::prelude::*;

fn main() {
    launch(app);
}

fn app() -> impl IntoElement {
    // Create a piece of reactive state
    // This holds a number that starts at 0
    let mut count = use_state(|| 0);

    // The main container
    rect()
        .expanded()
        .background(Color::from_rgb(30, 30, 30))
        .main_align_center()
        .cross_align_center()
        // Arrange children vertically (top to bottom)
        .direction(Direction::Vertical)
        // Add 16 pixels of space between children
        .gap(16.0)
        // The count display
        .child(
            label()
                .font_size(48.0)
                .color(Color::WHITE)
                // count.read() gets the current value
                .text(format!("Count: {}", *count.read()))
        )
        // The button container
        .child(
            rect()
                // Arrange buttons horizontally (left to right)
                .direction(Direction::Horizontal)
                .gap(8.0)
                // Minus button
                .child(
                    Button::new()
                        // When clicked, decrease the count
                        .on_press(move |_| *count.write() -= 1)
                        .child("-")
                )
                // Plus button
                .child(
                    Button::new()
                        // When clicked, increase the count
                        .on_press(move |_| *count.write() += 1)
                        .child("+")
                )
        )
}

Key Concepts in the Counter

Reactive State

let mut count = use_state(|| 0);

[!NOTE] Why || 0 instead of just 0? use_state needs to know how to create the initial value. Using a closure (||) lets Freya call it only when needed. For simple values it seems unnecessary, but it’s consistent with more complex cases.

Reading State

*count.read()

Writing State

*count.write() -= 1
*count.write() += 1

Closures in Event Handlers

.on_press(move |_| *count.write() -= 1)

Built-in Components

Button::new()
    .on_press(...)
    .child("-")

Layout Direction

.direction(Direction::Vertical)   // Stack top to bottom
.direction(Direction::Horizontal) // Stack left to right

This controls how multiple children are arranged within a container.

Gap Between Elements

.gap(16.0)

Adds 16 pixels of space between each child element.

Project Structure

As your app grows, organize it like this:

my-app/
├── Cargo.toml
└── src/
    ├── main.rs              # Entry point with launch()
    ├── components/          # Reusable UI components
    │   ├── mod.rs
    │   ├── button.rs
    │   └── card.rs
    ├── hooks/               # Custom stateful logic
    │   ├── mod.rs
    │   └── use_counter.rs
    └── themes/              # Theme definitions
        └── mod.rs

The mod.rs Files

Each directory needs a mod.rs that declares what’s in that folder:

// src/components/mod.rs
pub mod button;
pub mod card;

Defining Components

Function Components (Simplest)

Use these for components without props:

fn header() -> impl IntoElement {
    rect()
        .padding(16.0)
        .background(Color::BLUE)
        .child(label().text("My App"))
}

Struct Components (With Props)

Use these when you need to pass data:

// Define the component with its properties
#[derive(PartialEq)]
struct Greeting {
    name: String,
}

// Implement the Component trait
impl Component for Greeting {
    // The render method returns the UI
    fn render(&self) -> impl IntoElement {
        rect()
            .padding(8.0)
            .child(label().text(format!("Hello, {}!", self.name)))
    }
}

// Using the component
fn app() -> impl IntoElement {
    rect()
        .child(Greeting { name: "World".to_string() })
}

[!NOTE] Why #[derive(PartialEq)]? Freya needs to know when a component’s props have changed so it can optimize re-renders. PartialEq lets Freya compare old and new props efficiently.

Common Mistakes & Solutions

Mistake 1: Forgetting .expanded()

Problem: Window is blank or very small.

rect()
    // Missing .expanded() - rect has zero size!
    .child(label().text("Hello"))

Solution: Always give your root element a size:

rect()
    .expanded()  // Fill the window
    .child(label().text("Hello"))

Mistake 2: Not Using move in Closures

Problem: Compilation error about lifetimes.

let count = use_state(|| 0);
.on_press(|_| *count.write() += 1)  // Error!

Solution: Add move:

let count = use_state(|| 0);
.on_press(move |_| *count.write() += 1)  // Works!

Mistake 3: Forgetting mut for Mutable State

Problem: Can’t modify state.

let count = use_state(|| 0);  // Missing mut
.on_press(move |_| *count.write() += 1)  // Error!

Solution: Add mut:

let mut count = use_state(|| 0);  // Now mutable

Freya Architecture Overview

Freya consists of several crates working together:

CratePurpose
freyaMain crate - imports everything you need
freya-coreCore elements, hooks, events, lifecycle
freya-componentsBuilt-in UI components (Button, Input, etc.)
freya-animationAnimation system
freya-routerNavigation between pages
freya-radioGlobal state management
freya-i18nInternationalization/translations
torinThe layout engine (positions elements)

[!TIP] You don’t need to worry about these internal crates! use freya::prelude::* gives you access to everything.

Troubleshooting

”Build fails on Linux”

Make sure you’ve installed all the system dependencies listed earlier. Different Linux distributions package things differently.

”Window appears but is blank”

  1. Make sure your root element has a size (use .expanded())
  2. Check that child elements have content
  3. Verify colors are visible (white text on white background = invisible)

“Changes aren’t showing”

  1. Make sure you saved the file
  2. If not using hot-reload, restart the app (cargo run)
  3. Check the terminal for error messages

”Compilation is slow”

The first build is always slow (can take 2-5 minutes). This is because Rust is compiling Freya and all its dependencies. Subsequent builds are much faster (seconds).

Summary

In this tutorial, you learned:


Next: Part 2: Core Elements →

In the next tutorial, we’ll dive deep into the core elements that make up every Freya application: rect, label, paragraph, svg, and image. You’ll learn how to combine these building blocks to create complex, beautiful interfaces.