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
| Type | What It Is | Example |
|---|---|---|
| CLI (Command Line Interface) | Text-based interaction in a terminal | cargo run, git commit |
| GUI (Graphical User Interface) | Visual windows with clickable elements | VS 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:
- Create a Window - The operating system gives you a rectangular area to draw in
- Draw UI Elements - You tell the computer what to display (buttons, text, images)
- Handle Events - When users click, type, or scroll, your app responds
- Update Display - When data changes, redraw the screen to reflect it
- 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:
- Framework: A collection of tools and patterns that help you build applications faster
- Cross-platform: The same code works on Windows, macOS, and Linux
- GUI: It helps you create visual, interactive applications
- Rust: You write code in the Rust programming language
The Technology Stack
Freya is built on two powerful technologies:
- Dioxus - A Rust framework for building user interfaces (similar to React in JavaScript)
- 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?
| Feature | What It Means For You |
|---|---|
| GPU Accelerated | Your apps run smoothly without lag |
| Reactive State | When data changes, the UI updates automatically |
| Declarative UI | You describe what the UI should look like, not how to draw it |
| Cross-Platform | Write once, run on Windows/Mac/Linux |
| Built-in Components | Buttons, inputs, sliders - all ready to use |
| Theming Support | Easy light/dark mode and custom colors |
| Animation System | Smooth transitions and effects |
| Accessibility | Works with screen readers and keyboard navigation |
Freya vs Other Options
| Framework | Language | Rendering | Best For |
|---|---|---|---|
| Freya | Rust | Skia (GPU) | Native desktop apps with Rust |
| Tauri | Rust + Web | WebView | Apps with web technologies |
| Electron | JavaScript | Chromium | Web developers |
| Flutter | Dart | Skia | Mobile + Desktop |
| Qt | C++ | Native | Enterprise 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
rustcandcargo?
rustcis the Rust compiler - it turns your code into executable programscargois Rust’s package manager - it manages dependencies, runs tests, and builds projects- You’ll mostly use
cargoday-to-day; it callsrustcautomatically
2. A Code Editor
VS Code with the rust-analyzer extension is highly recommended:
- Install VS Code
- Open Extensions (Ctrl+Shift+X)
- Search for “rust-analyzer” and install it
- Search for “CodeLLDB” and install it (for debugging)
3. Basic Rust Knowledge
You should be familiar with:
- Variables (
let x = 5;) - Functions (
fn do_something() { }) - Structs (
struct Point { x: i32, y: i32 }) - Enums (
enum Option<T> { Some(T), None }) - Closures (
|x| x * 2)
[!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-reloadduring 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::*;
useis Rust’s keyword for importingfreyais the crate namepreludeis a module containing commonly-used items*means “import everything”
This single line gives you access to launch, rect, label, Color, and many other useful types.
The main Function
fn main() {
launch(app);
}
fn main()is the entry point of every Rust programlaunch(app)starts the GUI event loopappis the name of our component function
[!NOTE] What’s an Event Loop? GUI applications need to continuously:
- Check for user input (mouse clicks, key presses)
- Update the display
- Handle system events
This is called an “event loop,” and
launch()sets it up for you automatically.
The Component Function
fn app() -> impl IntoElement {
fn app()defines a function namedapp()means it takes no parameters->indicates the return typeimpl IntoElementmeans “returns something that can be rendered”
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))
rect()creates a rectangular container element.expanded()is shorthand for “fill all available space”.background(...)sets the background color
[!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:
Color::from_rgb(r, g, b)- Red, Green, Blue (0-255)Color::from_hex("#ff0000")- Hex color codes- Predefined colors like
Color::RED,Color::WHITE, etc.
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:
- Main axis = vertical direction (in a vertical layout)
- Cross axis = horizontal direction (in a vertical layout)
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!")
)
.child(...)adds an element inside the rectlabel()creates a text element.font_size(32.0)sets the text size in points.color(Color::WHITE)sets the text color.text("Hello, Freya!")sets the actual text content
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);
use_statecreates state that Freya tracks|| 0is a closure that returns the initial value (0)- When this state changes, the UI automatically updates
mutmeans we can modify this state
[!NOTE] Why
|| 0instead of just0?use_stateneeds 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()
count.read()gets a reference to the value*dereferences it to get the actual number.read()also tells Freya “this component depends on this value”
Writing State
*count.write() -= 1
*count.write() += 1
count.write()gets mutable access to the value- We can then modify it with
+=or-= - After modification, Freya automatically re-renders the UI
Closures in Event Handlers
.on_press(move |_| *count.write() -= 1)
movecapturescountby value (required for closures)|_|is the parameter - the underscore means “I don’t care about the event details”- The body runs when the button is pressed
Built-in Components
Button::new()
.on_press(...)
.child("-")
Button::new()creates a button component.on_press(...)handles click/tap events.child("-")sets the button’s text content
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.PartialEqlets 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:
| Crate | Purpose |
|---|---|
freya | Main crate - imports everything you need |
freya-core | Core elements, hooks, events, lifecycle |
freya-components | Built-in UI components (Button, Input, etc.) |
freya-animation | Animation system |
freya-router | Navigation between pages |
freya-radio | Global state management |
freya-i18n | Internationalization/translations |
torin | The 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”
- Make sure your root element has a size (use
.expanded()) - Check that child elements have content
- Verify colors are visible (white text on white background = invisible)
“Changes aren’t showing”
- Make sure you saved the file
- If not using hot-reload, restart the app (
cargo run) - 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:
- What GUIs are and how they work (event loops, rendering)
- What Freya is - a Rust GUI framework using Dioxus and Skia
- How to set up a Freya project with Cargo
- Basic elements -
rect()for containers,label()for text - Reactive state -
use_statefor data that updates the UI - Event handling -
.on_press()for responding to clicks - Layout basics - direction, alignment, gaps
- Components - function components and struct components
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.