Freya 0.3

5/8/2025 - marc2332

hey

Hey again, this is the announcement for Freya v0.3.0, the latest release of my Rust GUI Framework.

Website Example Screenshot

This is https://freyaui.dev/ but made with Freya it self!

Source code in GitHub

Incremental Rendering

Previously any change in the UI caused a full rerender. Now, rendering happens incrementally so only those all parts in the UI that change are rerendered, leaving the the rest intact, this translate to better performance even when consdering the cost of the calculations from incremental rendering.

For the purpose of internal debugging, I added a feature called fade-cached-incremental-areas to make the incremental rendering more evident. The parts of the UI that are left intact slowly fade out as new incremental renders are applied (this was inspired by an Iced video I saw some time ago).

This is the counter example after me having moved my cursor a few times over the Decrease button. Incremental Rendering Counter Screenshot

Layout

Flex

Freya now supports Flex layouting by using a combo of attributes, here is an example:

fn app() -> Element {
    rsx!(
        rect {
            content: "flex", // Marks this element as a Flex container
            direction: "horizontal",
            rect {
                width: "flex(1)", // Use 25% of the parent space after excluding the text from below
                height: "fill",
                background: "red"
            }
            label {
                "Some text here!"
            }
            rect {
                width: "flex(3)", // Use 75% of the parent space after excluding the text from above
                height: "fill",
                background: "green"
            }
        }
    )
}

Flex Screenshot

Alignments

The new space-between / space-around / space-evenly mimic the behavior in CSS:

Alignments List Screenshot

Spacing attribute

The new spacing attribute is a small but nice addition, it allows you to specify the space between elements from their parent element:

fn app() -> Element {
    rsx!(
        rect {
            spacing: "10",
            for i in 0..6 {
                rect {
                    key: "{i}",
                    background: "rgb(25, 35, 45)",
                    width: "100%",
                    height: "50"
                }
            }
        }
    )
}

Spacing Screenshot

Global Position

Elements can now global positioned, meaning that they will be positioned starting at X:0 and Y:0 of the window and they will not affect any other sibling element.

So, like with absolute but where the relative point is the window and not the parent element.

Example:

fn app() -> Element {
    rsx!(
        rect {
            padding: "10",
            rect { // Notice how this uses the padding from the parent element
                height: "20%",
                width: "20%",
                background: "black",
                position: "absolute",
                position_top: "10",
                position_left: "10",
            }
            rect { // But this one doesn't
                height: "20%",
                width: "20%",
                background: "red",
                position: "global",
                position_top: "10",
                position_right: "10",
            }
        }
    )
}

Position Screenshot

Source code of the example in GitHub.

Styling

rect elements can now have multiple borders using the border attribute.

Example:

fn app() -> Element {
    rsx!(
        rect {
            main_align: "center",
            cross_align: "center",
            width: "fill",
            height: "fill",
            rect {
                width: "100",
                height: "100",
                border: "6 inner red, 5 inner orange, 4 inner yellow, 3 inner green, 2 inner blue, 1 inner purple",
            }
        }
    )
}

Border 1 Screenshot

Another Example:

fn app() -> Element {
    rsx!(
        rect {
            main_align: "center",
            cross_align: "center",
            width: "fill",
            height: "fill",
            rect {
                width: "100",
                height: "100",
                border: "15 inner linear-gradient(0deg,rgb(98, 67, 223) 0%,rgb(192, 74, 231) 33%,rgb(255, 130, 238) 66%, white 100%), 4 center radial-gradient(red 0%, blue 80%)",
            }
        }
    )
}

Border 2 Screenshot

Source code of a more complex example in GitHub.

Radial and conic gradients

Support for radial and conic gradients have been added.

Example of their syntax:

fn app() -> Element {
    let mut gradient = use_signal(|| GradientExample::Linear);

    let background = match *gradient.read() {
        GradientExample::Linear => {
            "linear-gradient(250deg, orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)"
        }
        GradientExample::Radial => {
            "radial-gradient(orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)"
        }
        GradientExample::Conic => {
            "conic-gradient(250deg, orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)"
        }
    };

    ...
}

Radial:

Radial gradient Screenshot

Conic:

Conic gradient Screenshot

Source code of the example in GitHub.

Images

import_image

With the import_image macro you can easily turn image files into components.

import_image!(RustLogo, "./rust_logo.png", {
    width: "auto",
    height: "auto",
    sampling: "trilinear",
    aspect_ratio: "min",
});

fn app() -> Element {
    rsx!(RustLogo {})
}

aspect_ratio and cover

Images before needed explicit sizing by the developer, this is now optional as images are by defauly sized according to their encoded size. You can still tweak this behavior with the new aspect_ratio attribute.

In addition to that, a new cover attribute has been added to center the image according to its aspect ratio and size.

Source code of the example in GitHub.

Cache Rendering

Images can now optionally cache their decoding at render-level by specifying a cache_key, this tells Freya to cache the image bytes and to not decode it again on the next frame.

static RUST_LOGO: &[u8] = include_bytes!("./rust_logo.png");

fn app() -> Element {
    rsx!(
        image {
            image_data: static_bytes(RUST_LOGO),
            width: "fill",
            height: "fill",
            aspect_ratio: "min",
            cache_key: "rust-logo",
        }
    )
}

This is used for example by NetworkImage with the url as a cache key.

Source code of an example in GitHub.

SVG

import_svg

With the import_svg macro you can easily turn .svg files into components.

import_svg!(Ferris, "./ferris.svg", {
    width: "70%",
    height: "50%"
});

fn app() -> Element {
    rsx!(Ferris {})
}

Source code of an example in GitHub.

fill: "current_color"

SVGs can now use fill: "current_color" to use also the inherited/used color as fill.

static SETTINGS: &[u8] = include_bytes!("./settings.svg");

fn app() -> Element {
    rsx!(
         svg {
            color: "red",
            fill: "current_color",
            width: "100%",
            height: "50%",
            svg_data,
        }
    )
}

Source code of an example in GitHub.

Misc

Scale Factor

Freya apps will now always be in sync with the OS configured scale factor. No need to close the app and reopen, it will update in live.

Virtualization

The VirtualScrollview component now does pre-rendering of the closest items in both the start and the end, making the scroll way smoother.

Text Editing

Emojis support and other special characters are supported while editing text.

Text Editing Screenshot

Other improvements like selecting with Control + Shift + Arrows have been added as well.

Theming

Themes are now composed of colors palletes and component themes rather than just component themes, this makes it easier to reuse colors across component themes.

This is how the Dark theme is defined now:

pub const DARK_THEME: Theme = Theme {
    name: "dark",
    colors: ColorsSheet {
        primary: cow_borrowed!("rgb(103, 80, 164)"),
        focused_primary_border: cow_borrowed!("rgb(223, 180, 255)"),
        secondary: cow_borrowed!("rgb(202, 193, 227)"),
        tertiary: cow_borrowed!("rgb(79, 61, 130)"),
        surface: cow_borrowed!("rgb(60, 60, 60)"),
        secondary_surface: cow_borrowed!("rgb(45, 45, 45)"),
        neutral_surface: cow_borrowed!("rgb(25, 25, 25)"),
        focused_surface: cow_borrowed!("rgb(15, 15, 15)"),
        opposite_surface: cow_borrowed!("rgb(125, 125, 125)"),
        secondary_opposite_surface: cow_borrowed!("rgb(150, 150, 150)"),
        tertiary_opposite_surface: cow_borrowed!("rgb(170, 170, 170)"),
        background: cow_borrowed!("rgb(20, 20, 20)"),
        focused_border: cow_borrowed!("rgb(110, 110, 110)"),
        solid: cow_borrowed!("rgb(240, 240, 240)"),
        color: cow_borrowed!("rgb(250, 250, 250)"),
        primary_color: cow_borrowed!("white"),
        placeholder_color: cow_borrowed!("rgb(210, 210, 210)"),
        highlight_color: cow_borrowed!("rgb(96, 145, 224)"),
    },
    ..BASE_THEME
};

Built-in Components style

The built-in components style have been refreshed with a more modern style.

The button component now has 3 variants of 1, Button, FilledButton, and OutlineButton. Buttons variants Screenshot

The Scrollbar design has been refreshed, it now floats over the content with a small width unless you hover near it, then it gets bigger and gets a semi-transparent background. Scroll Screenshot

Here there is a collection of some of the components, with a refreshed style:

Refreshed Components Screenshot

New Docs

Freya uses docs.rs more than ever, all elements, attributes and events have been documented and you can even see their docs when hovering them in your code editor.

If you happen to see something missing or not well-explained please open an issue or even feel free to send a Pull Request.

Also, the most important (more to come in the future) built-in components offer previews embedded in docs.rs so you can see how a component looks like before even using it.

Here is a docs-only gallery section with previews of them, you can also see the individual previews in their respective docs.

Components Gallery Screenshot

Dioxus 0.6

Freya now uses Dioxus 0.6, nothing important here honestly!

New components

AnimatedPosition

The AnimatedPosition component animates its inner content position across time, any layout change that could make its content move, will then be animated. For this it needs to know the width and height in advance.

fn app() -> Element {
    rsx!(
        AnimatedPosition {
            width: "110",
            height: "60",
            function: Function::Quad,
            duration: Duration::from_millis(250),
            rect {
                background: "red",
                width: "60",
                height: "110"
            }
        }
    )
}

Here for example, clicking on “Toggle” changes the direction of the cards container, thus changing the cards position:

Source code of the example in GitHub.

Here is another example of cards that can be dragged and dropped:

Source code of the example in GitHub.

GlobalAnimatedPosition

GlobalAnimatedPosition is very similar to AnimatedPosition but it works with any content from anywhere and anywhere. It requires an extra id value that is guaranted to not change across time for this given element.

Here is an example of a grid, where each element is identified by a number. It doesnt matter that the elements get shuffled because each one is identified, therefore we can know where it comes from and where it goes and thus animate the transition.

The implementation could be improved to make the animation more fluid though.

Source code:

fn app() -> Element {
    let mut grid = use_signal(|| Grid::new(5));
    rsx!(
        rect {
            spacing: "12",
            main_align: "center",
            cross_align: "center",
            width: "fill",
            height: "fill",
            // This context provider is what stores the positions
            // The generic type is the ID type used for the cells
            GlobalAnimatedPositionProvider::<usize> {
                Button {
                    onpress: move |_| grid.write().suffle(),
                    label {
                        "Shuffle"
                    }
                }
                rect {
                    spacing: "6",
                    for row in grid.read().cells.chunks(5) {
                        rect {
                            direction: "horizontal",
                            spacing: "6",
                            for cell in row {
                                GlobalAnimatedPosition::<usize> {
                                    key: "{cell.id:?}",
                                    width: "100",
                                    height: "100",
                                    function: Function::Expo,
                                    duration: Duration::from_millis(600),
                                    id: cell.id,
                                    rect {
                                        width: "100",
                                        height: "100",
                                        background: "rgb({cell.id * 6}, {cell.id * 8}, { cell.id * 2 })",
                                        corner_radius: "32",
                                        color: "white",
                                        main_align: "center",
                                        cross_align: "center",
                                        label {
                                            "{cell.id:?}"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    )
}

Complete source code of the example in GitHub.

SelectableText

New read-only component that allows selecting text to copy for example.

Simple example:

fn app() -> Element {
    rsx!(
        SelectableText {
            value: "You can select this looooooooooong text"
        }
    )
}

Text Selectable Screenshot

Source code of the example in GitHub.

OverflowedContent

Animate a long content that otherwise could not be displayed, in a small container.

Simple example:

fn app() -> Element {
    rsx!(
        Button {
            OverflowedContent {
                width: "100",
                rect {
                    direction: "horizontal",
                    cross_align: "center",
                    label {
                        "Freya is a cross-platform GUI library for Rust"
                    }
                }
            }
        }
    )
}

Source code of the example in GitHub.

ResizableContainer

ResizableContainer in combination with ResizablePanel and ResizablePanel makes it possible to have panels whose size can be resized by dragging thin bars (also called handles).

fn app() -> Element {
    rsx!(
        ResizableContainer { // This is where ours panels and handles will be defined, default direction is vertical
            ResizablePanel { // A resizable panel with a minimum size of 50
                initial_size: 50., // Custom initial size, default is 10
                label {
                    "Hello"
                }
            }
            ResizableHandle { } // A thin bar
            ResizablePanel {
                initial_size: 50.,
                ResizableContainer { // And inside this panel we have yet another container, but this time it is horizontal
                    direction: "horizontal",
                    ResizablePanel {
                        initial_size: 35.,
                        label {
                            "World"
                        }
                    }
                    ResizableHandle { }
                    ResizablePanel {
                        initial_size: 20.,
                        min_size: 20., // Custom minimum size, default is 4
                        label {
                            "!"
                        }
                    }
                }
            }
        }
    )
}

Source code of the example in GitHub.

Tooltip

Tooltip is now an standalone component you can use to show some text when a given content is hovered:

fn app() -> Element {
    rsx!(
        TooltipContainer {
            tooltip: rsx!(
                Tooltip {
                    text: "You can see me now!"
                }
            ),
            Button {
                label { "Hover me" }
            }
        }
    )
}

Tooltip Screenshot

Source code of the example in GitHub.

AnimatedRouter

This simplifies animating transitions between pages of a router.

Source code of the example in GitHub.

Animations API

The use_animation hook now offers fully typed animations, making it easier to use it. Before, the animated values were type-erased and so their capabilities were limited.

fn app() -> Element {
    // UseAnimation<AnimNum>
    let animation = use_animation(|conf| {
        conf.auto_start(true);
        AnimNum::new(0., 360.)
        .time(500)
        .ease(Ease::InOut)
        .function(Function::Expo)
    });
    // ReadOnly<AnimNum>, you can pass it to other components if you need
    let sequential = animation.get();

    // &AnimNum
    let anim_num = &*sequential.read();

    // f32
    let rotation: f32 = anim_num.into();

    rsx!(
        rect {
            width: "100",
            height: "100",
            rotate: "{rotation}deg",
            background: "rgb(0, 119, 182)"
        }
    )
}

An example of this is the new SequentialAnimation, an animated value that can animate N amount of values:

fn app() -> Element {
    let animations = use_animation(|conf| {
        conf.auto_start(true);
        AnimSequential::new([
            AnimNum::new(0., 360.)
                .time(500)
                .ease(Ease::InOut)
                .function(Function::Expo),
            AnimNum::new(0., 180.)
                .time(2000)
                .ease(Ease::Out)
                .function(Function::Elastic),
        ])
    });

    let sequential = animations.get();

    let rotate_a = sequential.read()[0].read();
    let rotate_b = sequential.read()[1].read();

    rsx!(
        rect {
            width: "100",
            height: "100",
            rotate: "{rotate_a}deg",
            background: "rgb(0, 119, 182)"
        },
        rect {
            width: "100",
            height: "100",
            rotate: "{rotate_b}deg",
            background: "rgb(0, 119, 182)"
        }
    )
}

Source code of the example in GitHub.

Devtools

The devtools got some quality of life improvements:

  • Support for keyboard navigation in the nodes tree
  • Persisting the style / layout tab when changing between nodes.
  • Rendering elements roles before their tags if available
  • Slightly better layout preview

Devtools Screenshot

Accessibility

Keyboard navigation

Navigating with the keyboard (Tab and Tab + Shift) should now work in more components and work better in ngeneral.

Out of the box accessibility

Previously, only elements provided with a a11y_id were to be marked as accessible, this has changed.

Now, all elements are marked as accessible out of the box, obviously tou will need to provide accessibility data via the a11y attributes.

This means that a11y_id has changed from being an opt-in attribute to now only being required if you want to have a reference to the element to use in combination with use_focus.

New Attributes

A massive amount of accessibility attributes have been added to Freya, they all start with a a11y_ prefix so make it easier to identify and use.

Some of these include (existing ones were renamed too):

  • a11y_id
  • a11y_role
  • a11y_auto_focus
  • a11y_expanded
  • a11y_hidden
  • a11y_required And lot more.

IME

IME support has also been improved, and should work better than before.

Accessibility IME Screenshot

Canvas Snapshots

When using the headless testing runner (freya-testing) to test freya components, you will be able to make snapshots of the UI canvas and saving them to the disk.

This can be very useful for when you want to debug something visually in a test.

As a matter of fact, I reused this same API to create the new embedded previews in docs.rs. See New Docs.

Example:

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

    rsx!(
        rect {
            onclick: move |_| count += 1,
            label {
                font_size: "100",
                font_weight: "bold",
                "{count}"
            }
        }
    )
}

#[tokio::main]
async fn main() {
    let mut utils = launch_test(app);

    // Initial render
    utils.wait_for_update().await;
    utils.save_snapshot("./snapshot_before.png");

    // Emit click event
    utils.click_cursor((100., 100.)).await;

    // Render after click
    utils.save_snapshot("./snapshot_after.png");
}

Before: Accessibility Before Screenshot

After: Accessibility After Screenshot

Source code of a more complex example in GitHub.

i18n

Not exactly linked to Freya but the way to go for i18n in Freya apps is now using dioxus-i18n.

Its as easy to use as simply calling the t!() macro in the UI when you need to translate some text, or using i18n() and set_language to make a language change.

Example:

#[allow(non_snake_case)]
fn Body() -> Element {
    let mut i18n = i18n();

    rsx!(
        rect {
            Button {
                onpress: move |_| i18n.set_language(langid!("en-US")),
                label { "English" }
            }
            Button {
                onpress: move |_| i18n.set_language(langid!("es-ES")),
                label { "Spanish" }
            }
            label { {t!("hello", name: "Dioxus")} }
        }
    )
}
fn app() -> Element {
    use_init_i18n(|| {
        I18nConfig::new(langid!("en-US"))
            .with_locale(Locale::new_static(
                langid!("en-US"),
                include_str!("./en-US.ftl"),
            ))
            .with_locale(Locale::new_static(
                langid!("es-ES"),
                include_str!("./es-ES.ftl"),
            ))
    });

    rsx!(Body {})
}
# en-US.ftl
hello_world = Hello, World!

hello = Hello, {$name}!

New Examples

Here is a list of just a few new cool examples I added since the last release.

Animated VirtualScrollview:

mvandevander

mvandevander Screenshot

Infinite List:

Speedometer:

todo:

Thanks!

I want tothanks to the people helping and contributing to the project (specially to Aiving and Robertas) and also to my GitHub Sponsors (gqf2008, piny4man and Lino Le Van)!

If you want to support the project financially you can do so through my GitHub Sponsors.

From now on

I think I will probably stop making these blog posts as they take me too much time to write, I want to move to a faster release schedule so I will instead focus on simply make better changelogs in the GitHub releases. If something is worth of a blog post I will do it tho!

Thanks for reading ! 👋