Build desktop GUI application with rust, based on ECS pattern, powered by Bevy game engine.

Simple

Designed with Bevy’s philosophy — widgets are just Rust functions.

Built-in Widgets

Includes useful widgets like buttons, modals, list views and more.

For High-Performance Apps

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

Installation

cargo add famiq

or adding this to Cargo.toml

[dependencies]
famiq = "0.2.7"

Famiq supports only bevy 0.15.x onward. Currently, the latest version of bevy is 0.15.3.

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 UI.

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) // required by Famiq
        .add_systems(Startup, setup_ui)
        .run();
}

fn setup_ui(
    mut commands: Commands,
    mut famiq_res: ResMut<FamiqResource>,
    asset_server: Res<AssetServer>
) {
    commands.spawn(Camera2d::default());

    // create a builder
    let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server);

    // simple text & button widgets
    let text = fa_text(&mut builder, "Hello world").build();
    let button = fa_button(&mut builder, "Press me").build();

    // add widgets to a container
    fa_container(&mut builder).children([text, button]).build();
}

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.

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.

There are methods provided by FamiqBuilder including:

🔵 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/
    
    let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
        .use_font_path("fonts/Some-font.ttf");
  • 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/
    
    let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
        .use_font_path("fonts/Some-font.ttf");

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

🔵 register_tooltip()

This method enable tooltip option for some widgets. Currently only fa_button and fa_circular support tooltip option.

Note

If use_font_path is called, register_tooltip must be called after use_font_path to ensure that the custom font is applied to the tooltip.

let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
    .use_font_path("fonts/Some-font.ttf") // if use_font_path is called
    .register_tooltip();

🔵 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.
// normal project
let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
    .use_style_path("assets/my-styles.json");

// multi crate/workspace
let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
    .use_style_path("path/to/sub-crate/assets/subcrate-style.json");

🔵 hot_reload()

This method will enable hot-reload. 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.

let mut builder = FamiqBuilder::new(&mut commands, &mut famiq_res, &asset_server)
    .hot_reload();

Types of widgets

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

  • Containable widgets can have child widgets and can also be nested inside other containable widgets, including fa_container, fa_listview and fa_modal.

  • Non-containable widgets cannot have children and must be placed inside a containable widget. including fa_text, fa_button, fa_text_input, fa_selection, fa_image, fa_circular and fa_progress_bar.

As you can see in the code above, fa_text & fa_button were placed inside fa_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.

// by id
let button = fa_button(&mut builder, "Press me").id("#button").build();

// by class or classes
let text_1 = fa_text(&mut builder, "Hello world").class("text important").build();
let text_2 = fa_text(&mut builder, "Hello mom").class("text").build();
{
  "#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.

Currently, id or class assigned to a widget is immutable.

How to write bevy styles in JSON file?

Famiq supports almost all UI styles provided by Bevy engine.

Interaction

There are 3 types of interaction

  • Pressed
  • Hovered
  • None (leaving hovered)

Famiq emits an event with type of FaInteractionEvent whenever one of the iteraction is matched. The event then can be read from bevy's EventReader.

Each event has a widget type flag.

pub struct FaInteractionEvent {
    pub entity: Entity,
    pub widget_id: Option<String>,
    pub interaction: Interaction,
    pub widget: WidgetType,
}

Available widget types

pub enum WidgetType {
    Button,
    Container,
    Text,
    FpsText,
    TextInput,
    TextInputTogglePasswordIcon,
    ListView,
    ListViewItem,
    Selection,
    SelectionChoice,
    Circular,
    ProgressBar,
    Image
}

Handle interaction

You can write a bevy system that runs in Update schedule to handle Famiq’s widgets interaction.

Example,

fn handle_button_press_system(mut events: EventReader<FaInteractionEvent>) {
    for e in events.read() {
        if !e.is_pressed(WidgetType::Button) {
            return;
        }

        // make sure this works only with widgets that have id provided
        if let Some(id) = e.widget_id.as_ref() {
            match id.as_str() {
                "#button-one" => {
                    // do something when #button-one is pressed
                },
                "#button-two" => {
                    // do something when #button-two is pressed
                }
                _ => ()
            }
        }
    }
}

Beside is_pressed, there are also is_hovered and is_left (from hovered -> none).

Built-in classes

Famiq provides built-in classes for color, size and spacing.

Color

is-primary | is-secondary | is-success | is-danger | is-info | is-warning |

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

is-dark.

color classes

Size

is-small | is-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.

Widgets

Famiq provides some default widgets that are likely required to build GUI application.

FaContainer

An empty and stylyable widget. Think of it as a div inside HTML. It can also be used to create custom widgets.

Note

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

  • If you use fa_container to create a custom widget without children, you must set the height to a specific value.

usage

let container = fa_container(&mut builder).build();

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

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • children([Entity])

Example

let boss = fa_text(&mut builder, "Hello Boss").build();
let mom = fa_text(&mut builder, "Hello Mom").build();

fa_container(&mut builder)
    .id("#my-container")
    .children([boss, mom])
    .build();

FaButton

Usage

let button = fa_button(&mut builder, "Press me").build();

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

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • color(&str): set custom background color.

Example

let default_btn = fa_button(&mut builder, "Default button")
    .id("#default-btn")
    .build();

let info_btn = fa_button(&mut builder, "Info button")
    .id("#info-btn")
    .class("is-info")
    .build();

fa_container(&mut builder)
    .children([default_btn, info_btn])
    .build();

Handle button press

You can write a system that run in Update schedule to handle button events (hovered, pressed, none).

fn handle_button_press_system(mut events: EventReader<FaInteractionEvent>) {
    for e in events.read() {

        // it's not a button press event, return early.
        if !e.is_pressed(WidgetType::Button) {
            return;
        }

        // make sure this works only with buttons that have id provided
        if let Some(id) = e.widget_id.as_ref() {
            match id.as_str() {
                "#default-btn" => {
                    // do something when default button is pressed
                },
                "#info-btn" => {
                    // do something when info button is pressed
                }
                _ => ()
            }
        }
    }
}

FaText

Usage

let text = fa_text(&mut builder, "Some text").build();

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

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • color(&str): set custom color.

Example

let boss = fa_text(&mut builder, "Hello Boss").build();
let mom = fa_text(&mut builder, "Hello Mom").build();

fa_container(&mut builder).children([boss, mom]).build();

FaFpsText

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

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

Usage

fa_fps(&mut builder).build();

return Entity.

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • change_color(): change number color based on its value.
  • right_side(): make FPS text appears at the top right corner.

Example

fa_fps(&mut builder).change_color().right_side().build();

FaTextInput

Widget that allow user to type in texts.

Currently, this widget is not interactive.

Usage

let input = fa_text_input(&mut builder, "Enter your name").build();

Return Entity which must be used inside a containable widget.

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • is_password(): mask all the character as *.
  • color(&str): set custom background color.

Example

// default
let input_default = fa_text_input(&mut builder, "Enter your name").build();

// password
let input_password = fa_text_input(&mut builder, "Enter your password")
    .is_password()
    .build();

fa_container(&mut builder)
    .children([input_default, input_password])
    .build();

Change event

Whenever fa_text_input's value changes, it emits an event called FaTextInputChangeEvent that contains updated value.

pub struct FaTextInputChangeEvent {
    pub entity: Entity,
    pub widget_id: Option<String>,
    pub new_value: String
}

Example,

fn my_system(mut events: EventReader<FaTextInputChangeEvent>) {
    for e in events.read() {
        // make sure this works only with text input that have id provided
        if let Some(id) = e.widget_id.as_ref() {
            match id.as_str() {
                "#text-input-one" => {
                    println!("{:?}", e.new_value);
                }
                "#text-input-two" => {
                    // do something with #text-input-two new value
                }
            }
        }
    }
}

FaSelection

Usage

let selection = fa_selection(&mut builder, "Select choice")
    .choices(["Choice 1", "Choice 2"])
    .build();

Return Entity which must be used inside a containable widget.

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • choices([&str])
  • color(&str): set custom background color.

Example

let plans = fa_selection(&mut builder, "Select plan")
    .choices(["Personal", "Team", "Enterprise"])
    .build();
);

let subscriptions = fa_selection(&mut builder, "Select subscription payment")
    .choices(["Weekly", "Monthly", "Annually"])
    .build();
);

fa_container(&mut builder).children([plans, subscriptions]).build();

Change event

Whenever fa_selection's value changes, it emits an event called FaSelectionChangeEvent that contains updated value.

pub struct FaSelectionChangeEvent {
    pub entity: Entity,
    pub widget_id: Option<String>,
    pub new_value: String
}

Example,

fn my_system(mut events: EventReader<FaSelectionChangeEvent>) {
    for e in events.read() {
        // make sure this works only with selection that have id provided
        if let Some(id) = e.widget_id.as_ref() {
            match id.as_str() {
                "#selection-one" => {
                    println!("{:?}", e.new_value);
                }
                "#selection-two" => {
                    // do something with #selection-two new value
                }
            }
        }
    }
}

FaCircular

A spinning circular.

Usage

let circular = fa_circular(&mut builder).build();

Return Entity which must be used inside a containable widget.

Available methods

  • id(&str)
  • class(&str)
  • size(f32): set custom size (width & height).
  • color(&str): set custom color.

Example

// default
let cir = fa_circular(&mut builder).build();

// warning & small
let warning_cir = fa_circular(&mut builder)
    .class("is-warning is-small")
    .build();

// primary & custom size
let primary_cir = fa_circular(&mut builder)
    .class("is-primary")
    .size(50.0)
    .build();

// custom color
let custom_color_cir = fa_circular(&mut builder)
    .color("cyan_500")
    .build();

fa_container(&mut builder)
    .children([cir, warning_cir, primary_cir, custom_color_cir])
    .build();

FaModal

Usage

fa_modal(&mut builder).build();

Example

let text = fa_text(&mut builder, "Hello from modal").build();
let close_btn = fa_button(&mut builder, "Close").build();

let container = fa_container(&mut builder)
    .children([text, close_btn])
    .build();

fa_modal(&mut builder).children([container]).build();

FaListView

Vertical scrollable container.

Note

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

Usage

let button = fa_button(&mut builder, "Press me").build();
let input = fa_text_input(&mut builder, "Enter your name").build();

fa_listview(&mut builder).children([input, button]).build();

return Entity.

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • scroll_height(f32)
  • children([Entity])

FaImage

Image widget.

Note

Currently, it supports only jpg and png format.

Usage

let image = fa_image(&mut builder, "path/to/image.jpg").build();

return Entity which must be used inside a containable widget.

Available methods

  • id(&str)
  • class(&str)
  • set_size(Val, Val): set custom size (width, height). If this method is not called, image's original width & height will be used.

Example

let famiq_logo = fa_image(&mut builder, "logo.png")
    .set_size(Val::Px(100.0), Val::Px(100.0))
    .build();

fa_container(&mut builder).children([famiq_logo]).build();

FaBgImage

A widget use to create background image that covers the entire window.

Only one fa_bg_image is allowed. Supports jpg and png only.

Usage

fa_bg_image(&mut builder, "path/to/image.jpg").build();

FaProgressBar

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

Usage

let bar = fa_progress_bar(&mut builder).build();

Return Entity which must be used inside a containable widget.

Available methods

  • id(&str)
  • class(&str)
  • display(&str)
  • color(&str): set custom color.
  • percentage(f32): set percentage value of the bar.

Example

// default
let default_bar = fa_progress_bar(&mut builder).build();

// info & large
let info_large_bar = fa_progress_bar(&mut builder)
    .class("is-info is-large")
    .build();

// warning & 50%
let warning_bar = fa_progress_bar(&mut builder)
    .percentage(50.0)
    .class("is-warning")
    .build();

fa_container(&mut builder)
    .children([default_bar, info_large_bar, warning_bar])
    .build();

Resources

Resource is used to store global data that can be accessed from any systems within the app.

Famiq provides resources to interact with widgets in many ways.

FaContainableResources

Used to add, insert and remove children from a containable widget (fa_container, fa_modal, fa_listview).

To use this resource on any containable widget, id must be provided.

see more info about Resources.

Add children

append children to a containable widget.

fn my_system(mut containable_res: ResMut<FaContainableResource>) {
    // some logic

    containable_res.add_children("#container-id", &[new_text, new_button]);
}

Insert children

Insert children at given index into containable widget.

fn my_system(mut containable_res: ResMut<FaContainableResource>) {
    // some logic
    //
    let index: usize = 2;
    containable_res.add_children("#container-id", index,  &[new_text, new_button]);
}

Remove children

Remove children from a containable widget.

You must store Entity of widgets that you want to remove later on in a resource, so that it is accessible from systems.

#[derive(Resource)]
struct MyResource(Vec<Entity>);

fn setup_ui(
    // other params
    mut commands: Commands
) {
    // some code ...

    let my_button = fa_button(&mut builder, "Press").build();
    commands.insert_resource(MyResource(vec![my_button]));
}

fn my_system(
    mut containable_res: ResMut<FaContainableResource>,
    my_resource: Res<MyResource>
) {
    // some logic

    containable_res.remove_children("#container-id", &my_resource.0);
}

FaTextInputResource

Use this resource to get value from fa_text_input.

To use this resource on fa_text_input, id must be provided.

fn my_system(mut input_res: ResMut<FaTextInputResource>) {
    // some logic

    let name: String = input_res.get_value("#name-input");
}

FaSelectionResource

Use this resource to get value from fa_selection.

To use this resource on fa_selection, id must be provided.

fn my_system(mut selection_res: ResMut<FaSelectionResource>) {
    // some logic

    let choice: String = selection_res.get_value("#selection-id");
}

FaTextResource

Use this resource to get or update fa_text's value.

To use this resource on fa_text, id must be provided.

fn my_system(mut text_res: ResMut<FaTextResource>) {
    // Some logic

    let title = text_res.get_value("#text-id");
    println!("{:?}", title); // How to play dota 2

    // update title
    text_res.update_value("#text-id", "How to play dota 2 in 2025");
}

FaModalState

Use this resource to show or hide fa_modal.

Only one fa_modal can be shown at a time.

To use this resource on fa_modal, id must be provided.

fn my_system(mut modal_state: ResMut<FaModalState>) {
    // Some logic

    modal_state.show("#welcome-modal");

    // or

    modal_state.hide("#other-modal");
}

FaStyleResource

Use this resource to update widget style directly within systems.

To use this resource on any widgets, id must be provided.

Currently, not all styles can be updated via this resource, but this will be improved in future.

Available methods

Change widget background color.

set_background_color(id: &str, color: &str)

Change widget border color.

set_border_color(id: &str, color: &str)

Change widget border radius "left right top bottom".

set_border_radius(id: &str, radius: &str)

Change widget width.

set_width(id: &str, width: &str)

Change widget height.

set_height(id: &str, height: &str)

Change widget visibility.

set_visibility(id: &str, visibility: &str)

Change widget margin "left right top bottom".

set_margin(id: &str, margin: &str)

Change widget padding "left right top bottom".

set_padding(id: &str, padding: &str)

Change widget display.

set_display(id: &str, display: &str)

Change text's color.

set_color(id: &str, color: &str)

Change text's font size.

set_font_size(id: &str, size: &str)

Change widget shadow color.

set_shadow_color(id: &str, color: &str)

FaBgImageResource

Use this resource to change window's background image.

fn my_system(mut bg_res: ResMut<FaBgImageResource>) {
    // some logic

    bg_res.change_image("path/to/image-in-assets.png");
}

Customising your app window

Currently, the only way to customise your window is via bevy's WindowPlugin.

see more about WindowPlugin.

Example

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

fn custom_window() -> WindowPlugin {
    WindowPlugin {
        primary_window: Some(Window {
            title: "Some title".into(),
            name: Some("Some app name".into()),
            resolution: (1000.0, 800.0).into(),
            resizable: false,
            ..default()
        }),
        ..default()
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(custom_window())) // set custom window into DefaultPlugins
        .add_plugins(FamiqPlugin)
        .add_systems(StartUp, setup_ui)
        .run();
}

fn setup_ui() {
    // some widgets ..
}

FPS

High fps means fast & smooth.

By default, bevy uses a present mode called Fifo, traditionally called "Vsync On". This synchronizes your app’s FPS with your monitor’s refresh rate.

So, no matter how powerful your GPU is, if your monitor runs at 60Hz, your app will be capped at 60 FPS.

To fully utilize your GPU and unlock higher frame rates, switch to the Immediate present mode in WindowPlugin. This removes the FPS cap but may cause screen tearing.

Example

use bevy::prelude::*;
use bevy::window::PresentMode; // make sure to import this
use famiq::prelude::*;

fn custom_window() -> WindowPlugin {
    WindowPlugin {
        primary_window: Some(Window {
            title: "Some title".into(),
            name: Some("Some app name".into()),
            resolution: (1000.0, 800.0).into(),
            resizable: false,
            present_mode: PresentMode::Immediate, // here
            ..default()
        }),
        ..default()
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(custom_window())) // set custom window into DefaultPlugins
        .add_plugins(FamiqPlugin)
        .add_systems(StartUp, setup_ui)
        .run();
}

fn setup_ui() {
    // some widgets ..
}