Welcome
Freya is work in progress cross-platform native GUI library for 🦀 Rust, built on top of 🧬 Dioxus and 🎨 Skia as graphics library.
Check out the examples in the Freya repository to learn more.
What is Freya?
Freya is native GUI library for Rust🦀, built on top of 🧬 Dioxus's core and powered by 🎨 Skia as a graphics library.
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 (experimental ⚠️)
- 🧩Compatible with dioxus-std and other Dioxus renderer-agnostic libraries
Why 🧬 Dioxus?
Dioxus is a React-like library for Rust. Its component and hooks model make it simple to use and scales to complex apps.
See it's differences with Freya.
Why 🎨 Skia?
Skia is a battle-tested and well-maintained graphics library, and there are even some rusty bindings.
Differences with Dioxus
Freya uses most of the core packages of Dioxus, but not all them.
These are the main differences between Freya and the different Dioxus renderers for Desktop (webview and Blitz):
| Category | Freya | Dioxus |
|---|---|---|
| Elements, attributes and events | Custom | HTML |
| Layout | Custom (torin) | WebView and taffy |
| Renderer | Skia | WebView or WGPU |
| Components library | Custom | None, but can use CSS libraries |
| Devtools | Custom | Provided in Webview |
| Headless testing runner | Custom | None |
Environment 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
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.
Getting started
I encourage you to learn how Dioxus works, when you are done you can continue here. Also make sure you have the followed the environment 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
Make sure to add Freya and Dioxus as dependencies:
[package]
name = "freya-app"
version = "0.1.0"
edition = "2021"
[dependencies]
freya = "0.1"
dioxus = { version = "0.4", features = ["macro", "hooks"] }
src/main.rs
And paste this code in your main.rs file.
#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] use freya::prelude::*; fn main() { launch(app); } fn app(cx: Scope) -> Element { let mut count = use_state(cx, || 0); render!( rect { height: "100%", width: "100%", background: "rgb(35, 35, 35)", color: "white", padding: "12", onclick: move |_| count += 1, label { "Click to increase -> {count}" } } ) }
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.
Elements
Freya contains a set of primitive elements:
rect
The rect element (aka rectangle) is a box where you can place as many elements inside you want.
You can specify things like width, paddings or even in what direction the inner elements are stacked.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { direction: "vertical", label { "Hi!" } label { "Hi again!"} } ) } }
label
The label element simply shows some text.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { "Hello World" } ) } }
svg
The svg element let's you draw a SVG. You will need to use the bytes_to_data to transform the bytes into data the element can recognize.
Example:
#![allow(unused)] fn main() { static FERRIS: &[u8] = include_bytes!("./ferris.svg"); fn app(cx: Scope) -> Element { let ferris = bytes_to_data(cx, FERRIS); render!( svg { svg_data: ferris, } ) } }
image
The image element, just like svg element, require you to pass the image bytes yourself.
#![allow(unused)] fn main() { static RUST_LOGO: &[u8] = include_bytes!("./rust_logo.png"); fn app(cx: Scope) -> Element { let image_data = bytes_to_data(cx, RUST_LOGO); render!( image { image_data: image_data, width: "{size}", height: "{size}", } ) } }
paragraph and text
Both paragraph and text elements are used together. They will let you build texts with different styles.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( paragraph { text { font_size: "15", "Hello, " } text { font_size: "30", "World!" } } ) } }
Layout
Learn how the layout attributes work.
width & heightmin_width & min_heightmax_width & max_heightSize unitsdirectionpaddingmargindisplay
⚠️ Freya's layout is still somewhat limited.
width & height
All elements support both width and height attributes.
See syntax for Size Units.
Usage
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { background: "red", width: "15", height: "50", } ) } }
min_width & min_height
rect supports specifying a minimum width and height, this can be useful if you use it alongside a percentage for the target size.
See syntax for Size Units.
Usage
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { background: "red", min_width: "100", min_height: "100", width: "50%", height: "50%", } ) } }
max_width & max_height
rect supports specifying a maximum width and height.
See syntax for Size Units.
Usage
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { background: "red", max_width: "50%", max_height: "50%", width: "500", height: "500", } ) } }
Size Units
Logical pixels
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { width: "50", height: "33" } ) } }
Percentages
Relative percentage to the parent equivalent value.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { width: "50%", // Half the parent height: "75%" // 3/4 the parent } ) } }
calc()
For more complex logic you can use the calc() function.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { width: "calc(33% - 60 + 15%)", // (1/3 of the parent minus 60) plus 15% of parent height: "calc(100% - 10)" // 100% of the parent minus 10 } ) } }
direction
Control how the inner elements will be stacked, possible values are horizontal, vertical (default) or both (default for text elements, e.g label, paragraph, text, etc).
Usage
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { width: "100%", height: "100%", direction: "vertical", rect { width: "100%", height: "50%", background: "red" }, rect { width: "100%", height: "50%", background: "green" } } ) } }
padding
Specify the inner paddings of an element. You can do so by three different ways, just like in CSS.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { padding: "25" // 25 in all sides padding: "100 50" // 100 in top and bottom, and 50 in left and right padding: "5 7 3 9" // 5 in top, 7 in right, 3 in bottom and 9 in left } ) } }
display
Control how the inner elements are displayed, possible values are normal (default) or center.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { width: "100%", height: "100%", direction: "both", display: "center", rect { width: "50%", height: "50%", background: "red" }, } ) } }
margin
Specify the margin of an element. You can do so by three different ways, just like in CSS.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { margin: "25" // 25 in all sides margin: "100 50" // 100 in top and bottom, and 50 in left and right margin: "5 7 3 9" // 5 in top, 7 in right, 3 in bottom and 9 in left } ) } }
Style
Learn how the style attributes work.
background
The background attribute will let you specify a color as the background of the element.
You can learn about the syntax of this attribute here.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { background: "red" } ) } }
Compatible elements: rect
shadow
The shadow attribute let's you draw a shadow outside of the element.
Syntax: <x> <y> <intensity> <size> <color>
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { shadow: "0 0 25 2 rgb(0, 0, 0, 120)" } ) } }
Compatible elements: rect
corner_radius & corner_smoothing
The corner_radius attribute let's you smooth the corners of the element, with corner_smoothing you can archieve a "squircle" effect.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { corner_radius: "10", corner_smoothing: "75%" } ) } }
Compatible elements: rect
border
You can add a border to an element using the border and border_align attributes.
bordersyntax:[width] <solid | none> [color].border_alignsyntax:<inner | outer | center>.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { border: "2 solid black", border_align: "inner" } ) } }
Compatible elements: rect
overflow
Specify how overflow should be handled.
Accepted values: clip | none.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { overflow: "clip" width: "100", height: "100%", rect { width: "500", height: "100%", background: "red", } } ) } }
Compatible elements: rect
Color syntax
The attributes that have colors as values can use the following syntax:
Static colors
rectbluegreenyellowblack(default forcolorattribute)graywhite(default forbackgroundattribute)orangetransparent
rgb() / hsl()
- With RGB:
rgb(150, 60, 20) - With RGB and alpha:
rgb(150, 60, 20, 70) - With HSL:
hsl(28deg, 80%, 50%) - With HSL and alpha:
hsl(28deg, 80%, 50%, 25%)
Inheritance
These are some attribute that are inherited from the element parents:
colorfont_familyfont_sizefont_stylefont_weightfont_widthline_heightalignmax_linesletter_spacingword_spacingdecorationdecoration_styledecoration_colortext_shadow
Font Style
Learn how the font style attributes work.
colorfont_familyfont_sizealignfont_stylefont_weightfont_widthline_heightmax_linesletter_spacingword_spacingdecorationdecoration_styledecoration_colortext_shadowtext_overflow
color
The color attribute let's you specify the color of the text.
You can learn about the syntax of this attribute in Color Syntax.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { color: "green", "Hello, World!" } ) } }
Another example showing inheritance:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( rect { color: "blue", label { "Hello, World!" } } ) } }
Compatible elements: label, paragraph, text
font_family
With the font_family you can specify what font do you want to use for the inner text.
Limitation: Only fonts installed in the system are supported for now.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { font_family: "Inter", "Hello, World!" } ) } }
Compatible elements: label, paragraph,
font_size
You can specify the size of the text using font_size.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { font_size: "50", "Hellooooo!" } ) } }
Compatible elements: label, paragraph, text
align
You can change the alignment of the text using the align attribute.
Accepted values: center, end, justify, left, right, start
Example
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { align: "right", "Hello, World!" } ) } }
Compatible elements: label, paragraph,
font_style
You can choose a style for a text using the font_style attribute.
Accepted values: upright (default), italic and oblique.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { font_style: "italic", "Hello, World!" } ) } }
Compatible elements: text, label.
font_weight
You can choose a weight for a text using the font_weight attribute.
Accepted values:
invisiblethinextra-lightlightnormal(default)mediumsemi-boldboldextra-boldblackextra-black50100200300400500600700800900950
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { font_weight: "bold", "Hello, World!" } ) } }
Compatible elements: text, label.
font_width
You can choose a width for a text using the font_width attribute.
Accepted values:
ultra-condensedextra-condensedcondensednormal(default)semi-expandedexpandedextra-expandedultra-expanded
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { font_weight: "bold", "Hello, World!" } ) } }
Compatible elements: text, label.
line_height
Specify the height of the lines of the text.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { lines_height: "3", "Hello, World! \n Hello, again!" } ) } }
max_lines
Determines the amount of lines that the text can have. It has unlimited lines by default.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { "Hello, World! \n Hello, World! \n Hello, world!" // Will show all three lines } label { lines_height: "2", "Hello, World! \n Hello, World! \n Hello, world!" // Will only show two lines } ) } }
Compatible elements: text, paragraph.
letter_spacing
Specify the spacing between characters of the text.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { letter_spacing: "10", "Hello, World!" } ) } }
Compatible elements: text, paragraph, label.
word_spacing
Specify the spacing between words of the text.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { word_spacing: "10", "Hello, World!" } ) } }
Compatible elements: text, paragraph, label.
decoration
Specify the decoration in a text.
Accpted values:
underlineline-throughoverline
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { decoration: "line-through", "Hello, World!" } ) } }
Compatible elements: text, paragraph, label.
decoration_style
Specify the decoration's style in a text.
Accpted values:
solid(default)doubledotteddashedwavy
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { decoration: "line-through", decoration_style: "dotted", "Hello, World!" } ) } }
Compatible elements: text, paragraph, label.
decoration_color
Specify the decoration's color in a text.
You can learn about the syntax of this attribute in Color Syntax.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { decoration: "line-through", decoration_color: "orange", "Hello, World!" } ) } }
Compatible elements: text, paragraph, label.
text_shadow
Specify the shadow of a text.
Syntax: <x> <y> <size> <color>
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { text_shadow: "0 18 12 rgb(0, 0, 0)", "Hello, World!" } ) } }
Compatible elements: text, label.
text_overflow
Determines how text is treated when it exceeds its max_lines count. By default uses the clip mode, which will cut off any overflowing text, with ellipsis mode it will show ... at the end.
Accepted values:
clip(default)ellipsis
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { max_lines: "3", text_overflow: "ellipsis", "Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong text" } ) } }
Compatible elements: label, paragraph.
Effects
Learn how the effects attributes work.
rotate
The rotate attribute let's you rotate an element.
Example:
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( label { rotate: "180deg", "Hello, World!" } ) } }
Compatible elements: all except text.
Components
Freya comes with a set of ready to use components:
ButtonSwitchScrollViewVirtualScrollViewAccordionCanvasCursorAreaDrag and DropDropdownExternalLinkGestureAreaGraphInputLoaderNetworkImageSliderThemeProviderTooltip
Theming
Freya has built-in support for Theming.
⚠️ Currently, extending the base theme is not supported.
Accessing the current theme
You can access the whole current theme via the use_get_theme hook.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( ThemeProvider { Component { } } ) } #[allow(non_snake_case)] fn Component(cx: Scope) -> Element { let theme = use_get_theme(cx); let button_theme = &theme.button; render!( rect { background: "{button_theme.background}", } ) } }
Custom default theme
By default, the selected theme is DARK_THEME. You use the alternative, LIGHT_THEME or any you want.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( ThemeProvider { theme: LIGHT_THEME, Component { } } ) } #[allow(non_snake_case)] fn Component(cx: Scope) -> Element { let theme = use_get_theme(cx); let button_theme = &theme.button; render!( rect { background: "{button_theme.background}", } ) } }
Change theme
Changing the selected theme at runtime is possible by using the use_theme hook.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { render!( ThemeProvider { Component { } } ) } #[allow(non_snake_case)] fn Component(cx: Scope) -> Element { let theme = use_theme(cx); let onclick = |_| { *theme.write() = LIGHT_THEME; }; render!( Button { onclick: onclick, label { "Use Light theme" } } ) } }
Custom theme
Themes can be built from scratch or extended from others, like here with LIGHT_THEME:
#![allow(unused)] fn main() { const CUSTOM_THEME: Theme = Theme { button: ButtonTheme { background: "rgb(230, 0, 0)", hover_background: "rgb(150, 0, 0)", font_theme: FontTheme { color: "white" } }, ..LIGHT_THEME }; fn app(cx: Scope) -> Element { render!( ThemeProvider { theme: CUSTOM_THEME, rect { width: "100%", height: "100%", Button { label { "Report" } } } } ) } }
Hot reload
Freya supports Dioxus hot reload, this means you can update the layout and styling of your app on the fly, without having to compile any rust code.
Setup
Just before launching your app, you need to initialize the hot-reload context:
fn main() { dioxus_hot_reload::hot_reload_init!(Config::<FreyaCtx>::default()); launch(app); }
That's it!
Testing
Freya comes with a special testing renderer (freya-testing) that let's you run your components in a headless environment.
This will let you easily write unit tests for your components.
Getting started
You can use the launch_test function to run the tests of your component, it will return you a set of utilities for you to interact with the component.
For example, this will launch a state-less component and assert that it renders a label with the text "Hello World!".
#![allow(unused)] fn main() { #[tokio::test] async fn test() { fn our_component(cx: Scope) -> Element { render!( label { "Hello World!" } ) } let mut utils = launch_test(our_component); let root = utils.root(); let label = root.get(0); let label_text = label.get(0); assert_eq!(label_text.text(), Some("Hello World!")); } }
The root() function will give you the Root node of your app, then, with the get function you can retrieve a Node from it's parent given it's index position.
Dynamic components
If the component has logic that might execute asynchronously, you will need to wait for the component to update using the wait_for_update function before asserting the result.
Here, the component has a state that is false by default, but, once mounted it will update the state to true.
#![allow(unused)] fn main() { #[tokio::test] async fn dynamic_test() { fn dynamic_component(cx: Scope) -> Element { let state = use_state(cx, || false); use_effect(cx, (), |_| { state.set(true); async move { } }); render!( label { "Is enabled? {state}" } ) } let mut utils = launch_test(dynamic_component); let root = utils.root(); let label = root.get(0); assert_eq!(label.get(0).text(), Some("Is enabled? false")); // This will run the `use_effect` and update the state. utils.wait_for_update().await; assert_eq!(label.get(0).text(), Some("Is enabled? true")); } }
Events
We can also simulate events on the component, for example, we can simulate a click event on a rect and assert that the state has been updated.
#![allow(unused)] fn main() { #[tokio::test] async fn event_test() { fn event_component(cx: Scope) -> Element { let enabled = use_state(cx, || false); render!( rect { width: "100%", height: "100%", background: "red", direction: "both", onclick: |_| { enabled.set(true); }, label { "Is enabled? {enabled}" } } ) } let mut utils = launch_test(event_component); let rect = utils.root().get(0); let label = rect.get(0); utils.wait_for_update().await; let text = label.get(0); assert_eq!(text.text(), Some("Is enabled? false")); // Push a click event to the events queue utils.push_event(FreyaEvent::Mouse { name: "click", cursor: (5.0, 5.0).into(), button: Some(MouseButton::Left), }); // Run the queued events and update the state utils.wait_for_update().await; // Because the click event was executed and the state updated, now the text has changed too! let text = label.get(0); assert_eq!(text.text(), Some("Is enabled? true")); } }
Testing configuration
The launch_test comes with a default configuration, but you can also pass your own config with the launch_test_with_config function.
Here is an example of how to can set our custom window size:
#![allow(unused)] fn main() { #[tokio::test] async fn test() { fn our_component(cx: Scope) -> Element { render!( label { "Hello World!" } ) } let mut utils = launch_test_with_config( our_component, TestingConfig::default().with_size((500.0, 800.0).into()), ); let root = utils.root(); let label = root.get(0); let label_text = label.get(0); assert_eq!(label_text.text(), Some("Hello World!")); } }
Animating
Freya provides you with two hooks to help you animate your components.
use_animation
This a very simple hook that will let you animate a certain value from an inital value to a final value, in a given duration of time. There are a few animations that you can choose from:
- Linear
- EaseIn
- EaseInOut
- BounceIns
Here is an example that will animate a value from 0.0 to 100.0 in 50 milliseconds, using the linear animation.
fn main() { launch(app); } fn app(cx: Scope) -> Element { let animation = use_animation(cx, 0.0); let progress = animation.value(); use_effect(cx, (), move |_| { animation.start(Animation::new_linear(0.0..=100.0, 50)); async move {} }); render!(rect { width: "{progress}", }) }
use_animation_transition
This hook let's you group a set of animations together with a certain type of animation and a given duration. You can also specifiy a set of dependencies that will make animations callback re run.
Just like use_animation you have these animations:
- Linear
- EaseIn
- EaseInOut
- BounceIns
Here is an example that will animate a size and a color in 200 milliseconds, using the new_sine_in_out animation.
fn main() { launch(app); } const TARGET: f64 = 500.0; fn app(cx: Scope) -> Element { let animation = use_animation_transition(cx, TransitionAnimation::new_sine_in_out(200), (), || { vec![ Animate::new_size(0.0, TARGET), Animate::new_color("rgb(33, 158, 188)", "white"), ] }); let size = animation.get(0).unwrap().as_size(); let background = animation.get(1).unwrap().as_color(); let onclick = move |_: MouseEvent| { if size == 0.0 { animation.start(); } else if size == TARGET { animation.reverse(); } }; render!( rect { overflow: "clip", background: "black", width: "100%", height: "100%", offset_x: "{size}", rect { height: "100%", width: "200", background: "{background}", onclick: onclick, } } ) }
Virtualizing
Virtualizing helps you render a lot of data efficiently. It will only mount the elements you see in the screen, no matter how big the data is.
Target usages
- Text editor
- Tables
- Etc
Usage
Freya comes with a VirtualScrollView component which can help you archive the virtualization of some data.
The virtualization logic of
VirtualScrollViewis implemented at component-level, so, you could implement your own version if you wanted.
Here is an example:
fn main() { launch(app); } fn app(cx: Scope) -> Element { let values = use_state(cx, || vec!["Hello World"].repeat(400)); render!( VirtualScrollView { width: "100%", height: "100%", show_scrollbar: true, direction: "vertical", length: values.get().len(), item_size: 25.0, builder_values: values.get(), builder: Box::new(move |(key, index, _cx, values)| { let values = values.unwrap(); let value = values[index]; rsx! { label { key: "{key}", height: "25", "{index} {value}" } } }) } ) }
Parameters
show_scrollbar
By default, it does not display a scrollbar. However, you can enable it by setting the show_scrollbar parameter to true.
direction
It supports both vertical and horizontal directions. If direction is set to vertical, the items will be displayed in a single column, with the scrollbar appearing on the right-hand side. If direction is set to horizontal, the items will be displayed in a single row, with the scrollbar appearing at the bottom.
length
How many elements can be rendered. Usually the lenth of your data.
item_size
Used to calculate how many elements can be fit in the viewport.
builder_values
Any data that you might need in the builder function
builder
This is a function that dinamically creates an element for the given index in the list. It receives 4 arguments, a key for the element, the index of the element, the Scope (cx) and the builder_values you previously passed.
Devtools
Devtools can be enabled by adding the devtools to Freya.
// Cargo.toml
[dependencies]
freya = { version = "0.1", features = ["devtools"] }

