Overview

Freya is a work in progress cross-platform native GUI library for đŸĻ€ Rust, built on top of đŸ§Ŧ Dioxus and using 🎨 Skia as a graphics library.

#![allow(unused)]
fn main() {
fn app() -> Element {
    let mut count = use_signal(|| 0);

    rsx!(
        rect {
            height: "50%",
            width: "100%",
            main_align: "center",
            cross_align: "center",
            background: "rgb(0, 119, 182)",
            color: "white",
            shadow: "0 4 20 5 rgb(0, 0, 0, 80)",
            label {
                font_size: "75",
                font_weight: "bold",
                "{count}"
            }
        }
        rect {
            height: "50%",
            width: "100%",
            main_align: "center",
            cross_align: "center",
            direction: "horizontal",
            Button {
                onclick: move |_| count += 1,
                label { "Increase" }
            }
            Button {
                onclick: move |_| count -= 1,
                label { "Decrease" }
            }
        }
    )
}
}

Check out the examples in the Freya repository to learn more.

Features

  • ⛏ī¸ Built-in components (button, scroll views, switch and more)
  • 🚇 Built-in hooks library (animations, text editing and more)
  • 🔍 Built-in devtools panel (experimental ⚠ī¸)
  • 🧰 Built-in headless testing runner for components
  • 🎨 Theming support (not extensible yet ⚠ī¸)
  • 🛩ī¸ Cross-platform (Windows, Linux, MacOS)
  • đŸ–ŧī¸ SKSL Shaders support
  • 🔄ī¸ Dioxus Hot-reload support
  • 📒 Multi-line text editing (experimental ⚠ī¸)
  • đŸĻž Basic Accessibility Support
  • 🧩 Compatible with Dioxus SDK and other Dioxus renderer-agnostic libraries

Learn More

Setup

Make sure you have Rust and your OS dependencies installed.

Windows

You will need C++ build tools which you can get through Visual Studio 2022, learn more here.

Linux

Debian-based (Ubuntu, PopOS, etc)

Install these packages:

sudo apt install build-essential libssl-dev pkg-config cmake libgtk-3-dev libclang-dev

Arch Linux

Install these packages:

sudo pacman -S base-devel openssl cmake gtk3 clang

Fedora

Install these packages:

sudo dnf install openssl-devel pkgconf cmake gtk3-devel clang-devel -y
sudo dnf groupinstall "Development Tools" "C Development Tools and Libraries" -y

NixOS

Copy this flake.nix into your project root. Then you can enter a dev shell by nix develop.

Don't hesitate to contribute so other distros can be added here.

MacOS

No setup required. But feel free to add more if we miss something.

Custom Linkers

The following custom linkers are not supported at the moment:

  • mold

If there is another one not supported don't hesitate to add it here.

Learn Dioxus

This is a quick introduction to Dioxus in the world of Freya. For more examples or tips you can check the official Dioxus Docs or the Dioxus Cheatsheet

Components

Components in Dioxus are defined in the form of funtions that might receive some props and return an Element.

The Element is generated by the rsx!() macro.

fn MyComponent() -> Element {
    rsx!(...)
}

RSX

Dioxus uses a custom markup syntax called RSX, it's conceptually similar to React's JSX.

Syntax:

<Element Name> {
    <Element Attribute Name>: <Element Attribute Value>,
    <Element Children>
}

Example:

rsx!(
    label {
        onclick: |_| println!("Clicked"),
        color: "red",
        font_size: "50",
        "Hello, World!"
    }
)

Another Example:

rsx!(
    rect {
        color: "red",
        onclick: |_| println!("Clicked rect"),
        label {
            onclick: |_| println!("Clicked label"),
            font_size: "50",
            "Hello, World!"
        }
        AnotherComponent {
            some_value: 123
        }
    }
)

Props

Use the component macro if you want to have inlined props:

#[component]
fn MyComponent(name: String, age: u8) -> Element {
    rsx!(
        label {
            "{name} is {age} years old."
        }
    )
}

You can as well have a separate Struct for the props:

struct MyComponentProps {
    name: String,
    age: u8
}

fn MyComponent(props: MyComponentProps) -> Element {
    rsx!(
        label {
            "{props.name} is {props.age} years old."
        }
    )
}

State

Dioxus built-in state management uses Signals, and they are usually created with the use_signal hook.

fn MyComponent() -> Element {
    // `use_signal` takes a callback that initializes the state
    // No matter how many times the component re runs, 
    // the initialization callback will only run once at the first component run
    let mut state = use_signal(|| 0); 

    // Because signals are copy, we can move them into closures
    let onclick = move |_| {
        // Signals provide some mutation shortcuts for certain types
        state += 1;
        // But we could do as well
        *state.write() += 1;
    };

    // You subscribe to a signal by calling it (`signal()`), 
    // calling the `read()` method, or just embedding it into the RSX.
    // Everytime the signal is mutated the component function will rerun
    // because it has been subscribed, this will end up producing a 
    // new `Element` with the updated counter.
    println!("{}", state());
    println!("{}", state.read());

    rsx!(
        label { 
            onclick,
            "State is {state}"
         }
    )
}

Shared State

Signals can be passed to other components so they can read/write to the same signal.

fn app() -> Element {
    let state = use_signal(|| 0);

    // We pass the signal through the context API
    // So `ThirdComponent` can consume
    use_context_provider(|| state);

    rsx!(
        SecondComponent {
            state // We can pass the signal as a prop as well
        }
        ThirdComponent {}
    )
}

#[component]
fn SecondComponent(mut state: Signal<usize>) -> Element {
    let onclick = move |_| {
        state += 1;
    };

    rsx!(
        label {
            onclick,
            "Second component: {state}"
        }
    )
}

#[component]
fn ThirdComponent() -> Element {
    // We consume the signal passed through `use_context_provider`
    let mut state = use_context::<Signal<usize>>();

    let onclick = move |_| {
        state += 1;
    };

    rsx!(
        label {
            onclick,
            "Third component: {state}"
        }
    )
}

Alternative State Management

There are other state management libraries with more granular control or with other goals that are worth checking out.

Getting Started

I encourage you to first learn about Dioxus, when you are done you can continue here. Also make sure you have the followed the Setup guide.

Now, let's start by creating a hello world project.

Creating the project

mkdir freya-app
cd freya-app
cargo init

Cargo.toml

Let's add Freya and Dioxus, but this last one with only 2 features selected as we don't want to pull unnecessary dependencies.

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

[dependencies]
freya = "0.2"
dioxus = { version = "0.5", features = ["macro", "hooks"], default-features = false }

src/main.rs

You pass your root (usually named app) component to the launch function, which will open the window and then render your component.

#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use freya::prelude::*;

fn main() {
    launch(app); // Be aware that this will block the thread
}

Let's define our root component:

#![allow(unused)]
fn main() {
fn app() -> Element {

    // RSX is a special syntax to define the UI of our components
    // Here we simply show a label element with some text
    rsx!(
        label { "Hello, World!" }
    )
}
}

Components in Freya are just functions that return an Element and might receive some properties as arguments.

Let's make it stateful by using Signals from Dioxus:

#![allow(unused)]
fn main() {
fn app() -> Element {
    // `use_signal` takes a callback that initializes the state
    // No matter how many times the component re runs, 
    // the initialization callback will only run once at the first component run
    let mut state = use_signal(|| 0); 

    // Because signals are copy, we can move them into closures
    let onclick = move |_| {
        // Signals provide some mutation shortcuts for certain types
        state += 1;
        // But we could do as well
        *state.write() += 1;
    };

    // You subscribe to a signal by calling it (`signal()`), 
    // calling the `read()` method, or just embedding it into the RSX.
    // Everytime the signal is mutated the component function will rerun
    // because it has been subscribed, this will end up producing a 
    // new `Element` with the updated counter.
    println!("{}", state());
    println!("{}", state.read());

    rsx!(
        label { 
            onclick,
            "State is {state}"
         }
    )
}
}

Running

Simply run with cargo:

cargo run

Nice! You have created your first Freya app.

You can learn more with the examples in the repository.

Devtools

Devtools can be enabled by enabling the devtools to Freya.

// Cargo.toml

[dependencies]
freya = { version = "0.2", features = ["devtools"] }

Publishing

⚠ī¸ Before publishing, you should consider removing insecure metadata. For example, images might have EXIF location data in them.

Freya produces a self-contained executable in target/release, so you can technically distribute that. However, you might want to create an installer instead. You can use executable packagers of your choice, but for a more automated and "Rusty" version, you can use cargo-packager, which is basically an abstraction over executable packagers, which you would have to set up yourself.

There is an example you can check out.

cargo-packager installation

Run:

cargo install cargo-packager --locked

Usage

Add this to your Cargo.toml:

[package.metadata.packager]
before-packaging-command = "cargo build --release" # Before packaging, packager will run this command.
product-name = "My App" # By default, the crate name will be shown, but you probably prefer "My App" over "my-app".

And run:

cargo packager --release

And there you go! You should now have an installer in target/release for your current OS. To publish your app on a different OS, see the next section, Configuration.

Configuration

We used a very bare-bones example, so make sure to check out all configuration options in the Config struct in the cargo-packager API docs. Note that underscores should be hyphens when you use TOML.

One crucial configuration field is formats. This is a list of installers that cargo-packager should generate, and by default, it's your current OS. You can have a look at the list on GitHub, or on the API docs.

Changing the executable icon on Windows

cargo-packager will change the icon for platforms other than Windows using the icons field, but it does not do it on Windows (yet?).

Anyway, the cargo-packager team recommends using winresource (as opposed to winres which is not maintained). Before using it, make sure that you have the requirements that are listed on its page.

Add it to your build dependencies in Cargo.toml:

[build-dependencies]
winresource = "0.1.7"

And add this to your build.rs file (make sure you link it in your Cargo.toml):

// Change this to your icon's location
const ICON: &str = "assets/icons/icon.ico";

fn main() {
    if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
        let mut res = winresource::WindowsResource::new();
        res.set_icon(ICON);
        res.compile().unwrap();
    }
}

To convert more common formats like .png or .jpg to an .ico, you can use imagemagick. Once installed, run magick convert your_icon.png icon.ico.

Optimizing

The "Optimizing" chapter in the Dioxus docs applies in Freya too. Note that WebAssembly-related tips are irrelevant.

Differences with Dioxus

Freya is built on top of the core crates from Dioxus, this means that you will effectively be creating Dioxus components, using RSX and hooks.

But, thanks to Dioxus being a renderer-agnostic library you will NOT be using JavaScript, HTML, OR CSS, or any other abstraction that ends up using one of those or anything close to web.

Freya does everything on its own when it comes to:

  • Elements
  • Styling
  • Layout
  • Events
  • Rendering
  • Testing
  • Built-in components and hooks,
  • Editing
  • Animating

And more. Dioxus only is only used to run the app components (hooks, lifecycle, state, rsx), everything else is managed by Freya.

Freya is not mean to be drop-in alternative to Dioxus renderers but as GUI library on its own.

Here is the list of the main differences between Freya and the official Dioxus renderers for Desktop (WebView and Blitz):

CategoryFreyaDioxus Renderers
Elements, attributes and eventsCustomHTML
LayoutCustom (Torin)CSS or Taffy
StylingCustomCSS
RendererSkiaWebView or WGPU
Components libraryCustom (freya-comonents)None, but can use HTML elements and CSS libraries
DevtoolsCustom (freya-devtools)Provided in Webview
Headless testing runnerCustom (freya-testing)None, but there is Playwright and similar

Frequently Asked Questions

How is this different from Dioxus?

See the differences.

Will Freya have Mobile/Web support?

Freya's current focus is on Desktop (Windows, Linux, MacOS), so there are currently no plans to support either Mobile (Android/iOS) or Web platforms. But, this doesn't mean it won't happen in the future, who knows! From a technical point of view, it is possible to run Freya on these platforms with the right adjustments.

Why choose Skia instead of Webview?

These are the main reasons for this:

  • Ability to define the elements, attributes, styling, layout and events to my own criteria
  • App UIs look the same across platforms
  • Because Freya has control over the entire pipeline, it is easier to implement and use certain features such as headless testing runners
  • No reliance on OS for new features or fixes

Contact