Experimental GUI library, powered by Bevy engine.

Built-in Widgets

Includes useful widgets like button, dialog, text_input and more.

JSON-based styling

Write your styles in json file, keep your rust code clean. Any changes made to json file will reflect the running app immediately without needing to re-compile.

For High-Performance Apps

Leverages Bevy’s massive parallelism for smooth and efficient rendering.

Reactivity

Simple & lightweight, yet useful reactive system.

Installation

cargo add famiq

or adding this to Cargo.toml

[dependencies]
famiq = "0.3.1"

Famiq supports only bevy 0.16.x onward.

see crateio & rustdoc.

⚠️ IMPORTANT

Famiq is built on top Bevy and relies entirely on its ECS architecture. When you use Famiq, you're actually using the Bevy engine, with Famiq providing an abstraction for its UI system to help you build GUI applications, rather than Game.

A solid understanding of Bevy's ECS is required. If you're new to Bevy, I recommend checking out Bevy's quick start guide.

Getting Start

use bevy::prelude::*;
use famiq::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(FamiqPlugin::new()) // required by Famiq
        .add_systems(Startup, setup_ui)
        .run();
}

fn setup_ui(mut fa_query: FaQuery, mut famiq_res: ResMut<FamiqResource>) {
    // inject builder
    FamiqBuilder::new(&mut fa_query, &mut famiq_res).inject();

    // simple text & button
    container!(
        children: [
            text!(text: "Hello world"),
            button!(text: "Press me")
        ]
    );
}

run your project cargo run, you will see a text and a button.

What is FamiqPlugin?

FamiqPlugin brings in all the required Resources & Internal systems in order to run the app. It must be registered after DefaultPlugins provided by Bevy.

  • new(): use this method for default settings with 2d camera.
  • new_no_camera(): use this method if you want to spawn your own camera either 2d or 3d.

see FamiqPlugin.

What is FaQuery?

FaQuery is like document in HTML/Javascript. It allows us to interact with widgets including inserting reactive data, update reactive data, etc.

see FaQuery.

What is FamiqBuilder?

In simple terms, FamiqBuilder is the root UI node that acts as a starting point for building and managing widgets. All widgets are created and structured on top of this root. Before creating any UI widgets, FamiqBuilder must be created and injected.

There are methods provided by FamiqBuilder including:

see FamiqBuilder.

πŸ”΅ use_font_path()

By default, Famiq uses Fira mono regular as default font. To use another font, you can simply call use_font_path() method.

Example

  • For normal project structure:

    my_project/
    β”œβ”€β”€ assets/
    β”‚   β”œβ”€β”€ fonts/
    β”‚   β”‚   β”œβ”€β”€ Some-font.ttf
    β”œβ”€β”€ src/
    
    FamiqBuilder::new(&mut fa_query, &mut famiq_res)
        .use_font_path("fonts/Some-font.ttf")
        .inject();
  • For Multi-Crate/Workspace project structure: In a multi-crate workspace, the custom font path is read from the subcrate/member's assets/ folder:

    my_project/
    β”œβ”€β”€ sub_crate_1/
    β”‚   β”œβ”€β”€ assets/
    β”‚   β”‚   β”œβ”€β”€ fonts/
    β”‚   β”‚   β”‚   β”œβ”€β”€ Some-font.ttf
    β”‚   β”œβ”€β”€ src/
    β”œβ”€β”€ sub_crate_2/
    β”‚   β”œβ”€β”€ assets/
    β”‚   β”œβ”€β”€ src/
    
    FamiqBuilder::new(&mut fa_query, &mut famiq_res)
        .use_font_path("fonts/Some-font.ttf")
        .inject();

⚠️ some fonts might cause rendering issue including positioning and styling.

πŸ”΅ use_style_path()

By default, Famiq will look for json file for styling at assets/styles.json, relative to root directory. If you want to use another path or name, you can simply call use_style_path() method.

Note

  • For Multi-Crate/Workspace project structure: if you have json file inside sub-crate assets directory, you need to specify full path relative to root directory, not sub-crate.
  • Wasm build: if you target wasm, you need to explicitly call use_style_path(), where path is relative to assets directory.
// normal project
FamiqBuilder::new(&mut fa_query, &mut famiq_res)
    .use_style_path("assets/my-styles.json")
    .inject();

// wasm build
FamiqBuilder::new(&mut fa_query, &mut famiq_res)
    .use_style_path("my-styles.json")
    .inject();

// multi crate/workspace
FamiqBuilder::new(&mut fa_query, &mut famiq_res)
    .use_style_path("path/to/sub-crate/assets/subcrate-style.json")
    .inject();

πŸ”΅ hot_reload()

This method will enable hot-reload (exclude wasm). When it's enabled, every changes in json file will reflect the running app immediately without needing to re-compile the app. This should only be used during development.

FamiqBuilder::new(&mut fa_query, &mut famiq_res)
    .hot_reload()
    .inject();

πŸ”΅ inject()

This method must be called to inject builder into all widgets.

Types of widgets

There are 2 types of widgets provided by Famiq which are Containable widget and Non-containable widgets.

  • Containable widgets can have children and can also be nested inside other containable widgets, including container, scroll and dialog.

  • Non-containable widgets cannot have children and must be placed inside a containable widget. including text, button, text_input, selection, image, circular and progress_bar.

As you can see in the example code above, text & button were placed inside container.

How can I style my widgets?

Famiq uses a JSON-based styling system, similar to how HTML uses CSS.

Each widget can have an id or classes, which are used to apply styles from the JSON file.

Example

in your setup ui system:

// with id
let button = button!(text: "Press me", id: "#button");

// with class or classes
let text_1 = text!(text: "Hello world", class: "text important");
let text_2 = text!(text: "Hello Mom", class: "text");

in styles.json:

{
  "#button": {
    "background_color": "blue"
  },
  ".text": {
    "font_size": "40"
  },
  ".important": {
    "color": "red"
  }
}

Notes

  • IDs (id) must start with # and must match between the widget and the JSON file.
  • Class names (class) must start with . in the JSON file.

How to write bevy styles in JSON file?

Example

button!(text: "Press me", id: "#btn");
{
  "#btn": {
    "background_color": "blue"
  }
}

Styles

Famiq supports almost all UI styles provided by Bevy engine.

Events

Event is a big part of GUI library. So far, only FaMouseEvent is emitted by Famiq.

What is FaMouseEvent?

This event is emitted whenever one of the interaction is matched.

  • mouse-left down
  • mouse-right down
  • mouse up
  • mouse over
  • mouse out
  • mouse scroll

The event then can be read from bevy's EventReader.

Handle interaction

You can write a bevy system that runs in Update schedule to handle the events.

Example,

// register system
app.add_systems(Update, on_mouse_over_text);

// system
fn on_mouse_over_text(mut events: EventReader<FaMouseEvent>) {
    for e in events.read() {
        // not mouse over text, early return
        if !e.is_mouse_over(WidgetType::Text) {
            return;
        }

        // ok, now some text has mouse over it!

        // let's check which text
        if let Some(id) = e.id.as_ref() {
            match id.as_str() {
                "#welcome-text" => todo!(),
                "#other-text" => todo!(),
                _ => {}
            }
        }
    }
}

see

Built-in classes

Famiq provides built-in classes to save your time for color, size, alignment, border radius and spacing.

Color

primary | secondary | success | danger | info | warning |

primary-dark | success-dark | danger-dark | info-dark | warning-dark |

dark.

Size

small | large

Spacing

Margin

x is a number * 5px

  • mt-x | mb-x | ml-x | mr-x: margin top, bottom, left and right at x * 5px.
  • mt-auto | mb-auto | ml-auto | mr-auto: margin auto for top, bottom, left and right.
  • my-x: margin top and bottom at x * 5px.
  • my-auto: margin auto for top and bottom.
  • mx-x: margin left and right at x * 5px.
  • mx-auto: margin auto for left and right.

Padding

x is a number * 5px

  • pt-x | pb-x | pl-x | pr-x: padding top, bottom, left and right at x * 5px.
  • pt-auto | pb-auto | pl-auto | pr-auto: padding auto for top, bottom, left and right.
  • py-x: padding top and bottom at x * 5px.
  • py-auto: padding auto for top and bottom.
  • px-x: padding left and right at x * 5px.
  • px-auto: padding auto for left and right.

Border radius

  • rounded-0: 0px for all corners.
  • rounded-sm: 2px for all corners.
  • rounded-lg: 8px for all corners.
  • rounded-xl: 24px for all corners.
  • rounded-pill: 9999px for all corners.
  • rounded-circle: 50% for all corners.

default is 5px for all corners.

Alignment

JustifyContent

jc-start | jc-end | jc-flex-start | jc-flex-end | jc-center | jc-stretch | jc-space-between | jc-space-evenly | jc-space-around.

JustifyItems

ji-start | ji-end | ji-center | ji-stretch | ji-base-line.

JustifySelf

js-start | js-end | js-center | js-stretch | js-base-line.

AlignContent

ac-start | ac-end | ac-flex-start | ac-flex-end | ac-center | ac-stretch | ac-space-between | ac-space-evenly | ac-space-around.

AlignItems

ai-start | ai-end | ai-center | ai-stretch | ai-base-line ai-flex-start | ai-flex-end.

AlignSelf

as-start | as-end | as-center | as-stretch | as-base-line as-flex-start | as-flex-end.

Widgets

These are default widgets that are likely required to build GUI application.

Container

An empty and stylyable widget. Think of it as a div inside HTML.

Note

  • container! has its default height set to auto, meaning its height depends on its children.

usage

container!(id: "#my-container");

// or with children
container!(
    id: "#my-container",
    children: [
        text!(text: "Hello")
    ]
);

Return Entity of the widget which can optionally be used as child for another containable widget.

Available attributes

  • id
  • class
  • color
  • children

Button

Usage

let button = button!(text: "Press me");

Return Entity of the widget which must be used inside a containable widget.

Example

let default_btn = button!(text: "Default button", id: "#default-btn");
let info_btn = button!(text: "Info button", id: "#info-btn", class: "info");

container!(children: [default_btn, info_btn]);

Handle button press

fn handle_button_press(mut events: EventReader<FaMouseEvent>) {
    for e in events.read() {
        if e.button_press().is_none() {
            return;
        }

        match e.button_press().unwrap().as_str() {
            "#default-btn" => todo!(),
            "#info-btn" => todo!(),
            _ => {}
        }
    }
}

Required attribute

  • text

Available attributes

  • id
  • class
  • color

Text

Usage

let text = text!(text: "Some text");

Return Entity of the widget which must be used inside a containable widget.

Example

let boss = text!(text: "Hello Boss");
let world = text!(text: "Hello World");

container!(children: [boss, world]);

Required attribute

  • text

Available attributes

  • id
  • class
  • color

FpsText

Show FPS value at top-left corner of the window.

This widget doesn't need to be inside a containable widget.

Usage

fps!();

return Entity.

Example

fps!(right_side: true, change_color: false);

// or with reactive data
fa_query.insert_bool("right", true);
fa_query.insert_bool("can_change_color", false);

fps!(right_side: "$[right]", change_color: "$[can_change_color]");

Available attributes

  • id
  • class
  • color
  • right_side: show the fps at top-right corner.
  • change_color: change color based on its value.

TextInput

Widget that allow user to type in texts.

Note

  • Support single line only.
  • On web, clipboard is not supported yet.
  • model attribute is required.

Usage

let input = text_input!(placeholder: "Enter your name", model: "name");

Return Entity which must be used inside a containable widget.

Example

fa_query.insert_str("name", "");

container!(
    children: [
        text!(text: "$[name]"),
        text_input!(placeholder: "Enter your name", model: "name")
    ]
);

Required attribute

  • placeholder
  • model: type string.

Available attributes

  • id
  • class
  • color

Selection

Single choice selection.

Usage

let selection = selection!(
    placeholder: "Select choice",
    model: "select",
    choices: ["choice 1", "choice 2"]
);

Return Entity which must be used inside a containable widget.

Example

fa_query.insert_str("plan", "");
fa_query.insert_str("subscription", "");

let plans = selection!(
    placeholder: "Select plan",
    model: "plan",
    choices: ["Personal", "Team", "Enterprise"]
);
let subscriptions = selection!(
    placeholder: "Select subscription payment",
    model: "subscription",
    choices: ["Weekly", "Monthly", "Annually"]
);
container!(children: [plans, subscriptions]);

Required attributes

  • placeholder
  • model: type string.

Available attributes

  • id
  • class
  • color
  • choices

Circular

A spinning circular.

Usage

let circular = circular!();

Return Entity which must be used inside a containable widget.

Example

// default
let cir = circular!();

// warning & small
let warning_cir = circular!(class: "warning small");

// primary & custom size
let primary_cir = circular!(class: "primary", size: 50.0);

// custom color
let custom_color_cir = circular!(color: "cyan_500");

container!(
    children: [
        cir,
        warning_cir,
        primary_cir,
        custom_color_cir
    ]
);

Available attributes

  • id
  • class
  • color
  • size

Dialog

Usage

dialog!();

Example

fa_query.insert_bool("show_dialog", false);

dialog!(
    model: "show_dialog",
    children: [
        container!(children: [
            text!(text: "Hello from dialog"),
            button!(text: "Close")
        ])
    ]
);

Required attribute

  • model: type bool.

Available attributes

  • id
  • class
  • clear_bg: if true, the background is fully transparent

Scroll

Vertical scrollable container.

Note

scroll! has height set to 50% of the window or its parent container height.

Usage

let button = button!(text: "Press me");
let input = text_input!(placeholder: "Enter your name", model: "name");

scroll(children: [input, button]);

return Entity.

Available attributes

  • id
  • class
  • color
  • scroll_height: default to 15

Image

Image widget.

supports only jpg and png format.

Usage

let image = image!(path: "path/to/image.jpg");

return Entity which must be used inside a containable widget.

Example

container!(children: [
    image!(path: "logo.png", width: "100px", height: "100px")
]);

// or
fa_query.insert_str("logo_path", "path/to/image.jpg");

container!(children: [
    image!(path: "$[logo_path]", width: "100px", height: "100px")
]);

Required attribute

  • path: path to image relative to assets directory.

Available attributes

  • id
  • class
  • color
  • width
  • height

ProgressBar

There are 2 types of progress bar, Normal & Indeterminate. Default is Indeterminate.

Usage

let bar = progress_bar!();

Return Entity which must be used inside a containable widget.

Example

// default
let default_bar = progress_bar!();

// info & large
let info_large_bar = progress_bar!(class: "info large");

// warning & 50%
fa_query.insert_fnum("percent", 50.0);
let warning_bar = progress_bar!(class: "warning", model: "percent");

container!(children: [
    default_bar,
    info_large_bar,
    warning_bar
]);

Available attributes

  • id
  • class
  • color
  • model: if model is not provided it will be rendered as indeterminate. Type f32.

Reactivity

Famiq's reactivity is really simple, thus it's limited.

Data <-> Subscribers

When a data is changed, all the subscribers re-build themselve.

Unsupported features

  • template iteration
  • template condition

Example

fn setup_ui(mut fa_query: FaQuery, mut famiq_res: ResMut<FamiqResource>) {
    fa_query.insert_num("count", 0);

    container!(
        children: [
            text!(text: "Count: $[count]"),
            button!(text: "Increase", id: "#btn")
        ]
    );
}

fn on_button_press(
    mut events: EventReader<FaMouseEvent>,
    mut fa_query: FaQuery
) {
    for e in events.read() {
        if !e.is_button_pressed("#btn") {
            continue;
        }
        if let Some(count) = fa_query.get_data_mut("count") {
            let count = count.as_num_mut();
            *count += 1;
        }
    }
}

Inserting data

We start by inserting reactive data via FaQuery and the only supported data types are: i32, f32, bool, Vec<String>, and String.

  • insert_num: for inserting data as i32.
  • insert_fnum: for inserting data as f32.
  • insert_str: for inserting data as String.
  • insert_bool: for inserting data as boolean.
  • insert_list: for inserting data as Vector of String.
fa_query.insert_num("count", 0);

0 will be converted to RVal::Num(0).

Subscribing to data

We now can use the data in other widgets via reactive template $[].

fa_query.insert_num("count", 0);
fa_query.insert_str("name", "");
fa_query.insert_str("text_color", "blue");
fa_query.insert_bool("can_change_color", true);

fps!(change_color: "$[can_change_color]");

container!(children: [
    text!(text: "Counter: $[count]", color: "$[text_color]"),
    text!(text: "Name: $[name]"),
    text_input!(placeholder: "Enter name", model: "name")
]);

Getting & Mutating the data

getting data

if let Some(count) = fa_query.get_data("count") {
    println!("count is {:?}", count.as_num());
}
// or
let count = fa_query.get_data("count").unwrap();
println!("I'm sure count is available and it is {:?}", count.as_num());

mutating data

There are 2 ways to mutate a data.

  1. using get_data_mut
// in system

if let Some(count) = fa_query.get_data_mut("count") {
    let count = count.as_num_mut();
    *count += 1;
}

if let Some(state) = fa_query.get_data_mut("can_change_color") {
    let state = state.as_bool_mut();
    *state = false;
}
  1. explicitly calling mutate methods
// in system

fa_query.mutate_num("count", 2);
fa_query.mutate_bool("can_change_color", false);

Model

Inspired by Vue.js, model is a two-way binding between an input and reactive string.

fa_query.insert_str("name", "Foo");

text!(text: "My name is $[name]");

text_input!(placeholder: "Enter your name", model: "name");

Here, text widget subscribes to reactive data name. When the user types in text_input, text widget rebuild itself.