Fynix

A backend agnostic reactive view tree, styled the way Typst styles a document.

Free and open-source forever!

The Bevy Fynix gallery example: buttons in each look, a keyed list, popups and docked panes

The Bevy backend’s gallery example.


One View, Any Theme

A view reads token traits. Each card below is the same code under a different theme.

Rust
/// One card, written once. Its colours, sizes, corners and curves
/// all come from the theme it is built with.
fn card<T: Theme>() -> impl View<Web, T> {
    column((
        label("Deploy to production").bold(true),
        label("All checks passed.").tone(Tone::Dim),
        row((
            button(label("Later")).rules(ghost),
            button(label("Deploy")).rules(primary),
        ))
        .justify(Justify::End),
    ))
    .rules(panel)
    .transition(Motion::Interact)
}

/// The same card under three themes.
fn mount(
    dark: &mut Ui<Monokai>,
    light: &mut Ui<Paper>,
    sweet: &mut Ui<Candy>,
) {
    dark.mount(card());
    light.mount(card());
    sweet.mount(card());
}
Running in Your Browser

Set and Show Rules

Set rules fill what the call site left unset. Show rules win over it. Both end with their scope, as in Typst.

Rust
fn rules(cx: &mut Cx<'_, Web, Monokai>) {
    cx.scope(|cx| {
        // Every label from here on is small and dim...
        cx.set::<Label>(|l, _| l.size(12.0).tone(Tone::Dim));
        cx.build(label("A set rule: small and dim"));
        // ...unless its call site says otherwise.
        cx.build(label("The call site wins: big").size(22.0));
        // A show rule wins over the call site.
        cx.show::<Label>(|l, _| l.tone(Tone::Accent).bold(true));
        cx.build(
            label("A show rule wins: accent").tone(Tone::Critical),
        );
    });
    // The scope ended, and its rules with it.
    cx.build(label("After the scope: plain again"));
}
Running in Your Browser

Hover It

A state rule holds while its view is hovered or pressed, and .transition(..) makes the change glide.

Rust
fn buttons() -> impl View<Web, Monokai> {
    row((
        button(label("Cancel")).rules(ghost),
        button(label("Save")).rules(primary),
        button(label("Delete")).rules(danger),
        // A look of its own, written in place.
        button(label("Hover me"))
            .when_in::<Hovered, _>(|cx: &mut Cx<Web, Monokai>| {
                cx.root(|cx| {
                    cx.set::<Frame>(|f, t| {
                        f.fill(t.accent()).scale(1.1)
                    })
                });
                cx.set::<Label>(|l, _| l.tone(Tone::OnAccent));
            })
            .when_in::<Pressed, _>(|cx: &mut Cx<Web, Monokai>| {
                cx.root(|cx| cx.set::<Frame>(|f, _| f.scale(0.9)));
            }),
    ))
    .gap(12.0)
    .transition(Motion::Interact)
}
Running in Your Browser

Hover and press the buttons.

Lists That Animate Themselves

each keeps a view for every key that stays. New rows make room and fade in; dropped ones fade out and close the gap.

Rust
fn list() -> impl View<Web, Monokai> {
    column((
        row((
            button(label("Add")).rules(primary).on_click(|world| {
                let rows = world.get_mut::<Rows>();
                rows.ids.push(rows.next);
                rows.next += 1;
            }),
            button(label("Remove")).on_click(|world| {
                let rows = world.get_mut::<Rows>();
                if !rows.ids.is_empty() {
                    rows.ids.remove(0);
                }
            }),
            button(label("Reverse")).on_click(|world| {
                world.get_mut::<Rows>().ids.reverse()
            }),
        )),
        each(
            resource::<Rows, _>(|rows| rows.ids.clone()),
            |id| *id,
            |id| {
                item(*id)
                    .appear(hidden)
                    .transition(Motion::Expand)
                    .boxed()
            },
        )
        .within(column(()).gap(6.0)),
    ))
    .gap(14.0)
    .transition(Motion::Interact)
}
Running in Your Browser

Add, remove and reverse the rows.


Backend Agnostic

A backend says what a world and a node are. Everything above it runs unchanged, which is how this page runs Fynix on the DOM.

Build Your Own Backend →

Start With Bevy

Labels, frames, buttons in ready-made looks, popups and docking panes, on bevy_ui.

Set Up Bevy Fynix →
Bevy

Ready to Build?

Get Started →