Freya 0.3

5/25/2025 - marc2332

hey

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

It has been around 1 year since I last made the last release (v0.2) and blog post and a quite few things have changed so this blog post will be an overview of mostly user-facing changes that I considered worth mentioning, for a more complete changelog list you can check the v0.3 Release in GitHub.

Website Example Screenshot

This is https://freyaui.dev/, but made with Freya itself!

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 translates 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 1/4 (25%) of the parent space after excluding the text from below
                height: "fill",
                background: "red"
            }
            label {
                "Some text here!"
            }
            rect {
                width: "flex(3)", // Use 3/4 (75%) of the parent space after excluding the text from above
                height: "fill",
                background: "green"
            }
        }
    )
}

Flex Screenshot

Source code of another example in GitHub.

Alignments

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

  • space-between: Distributed among the available space
  • space-around: Distributed among the available space with small margins in the sides
  • space-evenly: Distributed among the available space with the same size of margins in the sides and in between the elements.

You can find more info in main_align attribute.

Example:

Note that each square has a margin of 10 pixels to make it more clear.

Alignments List Screenshot

Source code of the example in GitHub.

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

Source code of the example in GitHub.

Global Position

Elements can now be globally 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 it’s like absolute but where the element is relative to 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%)"
        }
    };

    rsx!(
        rect {
            background,
            ...
        }
    )
}

Radial:

Radial gradient Screenshot

Conic:

Conic gradient Screenshot

Source code of a more complete 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",
        }
    )
}

For example, this is used 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 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 at both the start and the end, making the scroll way smoother.

Text Editing

You can now use emojis and other special characters 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 color 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
};

Styling of built-in components

The style of the built-in components has been refreshed with a more modern look.

The button component now has 3 variants: 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

I have tried my best to make some decent docs, from introduction and getting started to API references.

There is still work to do but in my opinion it is a nice improvement over the previous releases.

You can find the different learning chapters in the docs.rs page.

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 built-in components offer previews embedded in docs.rs so you can see how a component looks like before even using it. More will be added in the future.

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, some memory-related issues has been fixed.

New components

AnimatedPosition

The AnimatedPosition component animates its inner content position over a set duration. 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 anytime. It requires an extra id value that should be guaranted not to change for the 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.

This is how the root component looks like:

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

SelectableText allows to render a text that can be selected but not written, useful to allow copying 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

OverflowedContent Allows to render a long text in a tiny space by animating it.

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 ResizableHandle 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 to resize
            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 and TooltipContainer can be used to show some text when hovering over the children of the component:

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

AnimatedRouter 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 AnimSequential, 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 general.

Out of the box accessibility

Previously, only elements provided with an a11y_id attribute were accessible.

But now, all elements are created with an internal Accessibility ID out of the box (you can still pass your own using a11y_id), but for them to be actually useful in terms of accessibility you will need to use 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 more control of the accessibility. Its like holding a reference to the element. It can be used with use_focus to e.g dinamically focus the element at any given moment or to check if it is being selected with the keyboard.

New Attributes

A massive amount of accessibility attributes have been added to Freya, they all start with a a11y_ prefix to make them easier to spot 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 a 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)[https://docs.rs/freya-testing/0.3.0/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 to thank to the people who has helped and contributed to the project (specially to Aiving, Robertas and Tropix126) 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 ! đź‘‹