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.
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):
Category | Freya | Dioxus Renderers |
---|---|---|
Elements, attributes and events | Custom | HTML |
Layout | Custom (Torin ) | CSS or Taffy |
Styling | Custom | CSS |
Renderer | Skia | WebView or WGPU |
Components library | Custom (freya-comonents ) | None, but can use HTML elements and CSS libraries |
Devtools | Custom (freya-devtools ) | Provided in Webview |
Headless testing runner | Custom (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