What is Famiq?

Famiq is a UI library built on top of Bevy UI by providing default widgets and a simple way to manage styles.

Instead of writing Rust code for styling, developers can define styles in a well known JSON file. These styles are then parsed into Bevy's native UI styles, significantly reducing boilerplate code.

Built on top of Bevy UI, based on Bevy ECS.

  • Simple: Follows Bevy's philosophyβ€”widgets are just Rust functions.
  • Clean: Styles can be defined in a JSON file, reducing boilerplate code.
  • Widgets: Includes useful default components like button, modal, listview and more.
  • Flexible: Just like in HTML/CSS, styles can be applied using id or classes.

Simple button

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

. &mut builder is a mutable reference of FamiqWidgetBuilder.

If you want to make changes to the widget, you can simply give it an id or class.

let my_btn = fa_button(&mut builder, "Press me").id("#my-btn").build();
{
  "#my-btn": {
    "background_color": "yellow"
  }
}

Hot reload

Hot-reload can be enabled during development. When it's enabled, every changes in json file will reflect the running app immediately without needing to re-compile the app.

let mut builder = FamiqWidgetBuilder::new(
    &mut commands,
    &mut builder_res,
    &asset_server
);

builder.hot_reload();

Bevy versions support

Famiq is new and still in early stage of development. Currently, it supports only 0.15.x onward.

Installation

Famiq can be installed by adding this line into Cargo.toml file.

cargo add famiq

or

[dependencies]
famiq = "0.2.5"

using the latest version is recommended.

Getting Start

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

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(FamiqPlugin) // add plugin
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut famiq_res: ResMut<FamiqWidgetResource>, // required
    asset_server: ResMut<AssetServer>, // required
) {
    commands.spawn(Camera2d::default());

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

    // create simple texts using the builder
    let hello_boss = fa_text(&mut builder, "Hello Boss").build();
    let hello_mom = fa_text(&mut builder, "Hello Mom").build();

    // add texts to container
    fa_container(&mut builder)
        .children([hello_boss, hello_mom])
        .build();
}

Hello Boss Screenshot

What is FamiqWidgetBuilder?

In simple terms, FamiqWidgetBuilder 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.

FamiqWidgetBuilder provides some useful methods:

πŸ”΅ 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/
    
    builder.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/
    
    // Inside subcrate 1
    builder.use_font_path("fonts/Some-font.ttf");z
    

⚠️ 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.

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

builder.hot_reload();

⚠️ hot-reload is expensive and it should be enabled only during development.

How styling works?

Bevy's default approach to UI development requires writing Rust code for styling, which can quickly become verbose and repetitive. Famiq introduces a way to define styles using JSON file, making UI development in Bevy more accessible and efficient.

Key Features

  • JSON-based Styling: Write styles in a familiar, CSS-like JSON format.
  • Automatic Parsing: JSON styles are parsed into Bevy's native style format.
  • Reduced Boilerplate: Eliminate repetitive Rust code for UI styling.
  • Hot-Reload: Any changes made to json file will be reflected to the running app without needing to re-compile the app.

Example

Normal Bevy UI styles.

commands.spawn((
    Node {
        border: UiRect::all(Val::Px(3.0)),
        padding: UiRect {
            left: Val::Px(5.0),
            right: Val::Px(5.0),
            top: Val::Px(10.0),
            bottom: Val::Px(10.0)
        },
        margin: UiRect::All(Val::Auto),
        width: Val::Percent(100.0),
        ..default()
    },
    BorderColor(Color::srgba(1.0, 1.0, 1.0, 0.3)),
    BorderRadius::all(Val::Px(5.0))
));

With Famiq, you can simply give widget an id or class, then write styles in json file.

{
  "#my-widget-id": {
    "padding": "5px 5px 10px 10px",
    "border": "3px 3px 3px 3px",
    "border_color": "srgba 1.0, 1.0, 1.0, 0.3",
    "border_radius": "5px 5px 5px 5px",
    "width": "100%",
    "margin": "auto auto auto auto"
  }
}

Supported & Unsupported styles

Unsupported

grid_template_rows: Vec<RepeatedGridTrack>
grid_template_columns: Vec<RepeatedGridTrack>
grid_auto_rows: Vec<GridTrack>
grid_auto_columns: Vec<GridTrack>
grid_row: GridPlacement
grid_column: GridPlacement

Supported

color: Color
font_size: f32

background_color: BackgroundColor
border_color: BorderColor
border_radius: BorderRadius
visibility: Visibility
z_index: ZIndex

display: Display
position_type: PositionType
overflow: Overflow
direction: Direction
left: Val
right: Val
top: Val
bottom: Val
width: Val
height: Val
min_width: Val
min_height: Val
max_width: Val
max_height: Val
aspect_ratio: Option<f32>
align_items: AlignItems
justify_items: JustifyItems
align_self: AlignSelf
justify_self: JustifySelf
align_content: AlignContent
justify_content: JustifyContent
margin: UiRect
padding: UiRect
border: UiRect
flex_direction: FlexDirection
flex_wrap: FlexWrap
flex_grow: f32
flex_shrink: f32
flex_basis: Val
row_gap: Val
column_gap: Val
grid_auto_flow: GridAutoFlow

Supported Val enum

Val {
    Auto,
    Px,
    Percent,
    Vw,
    Vh
}

Supported Color enum

Color {
    Srgba(Srgba),
    LinearRgba(LinearRgba),
    Hsla(Hsla)
}

How to write bevy styles in JSON file.

Famiq supports almost all UI styles provided by Bevy engine.

id & Class

{
  "#my-widget-id": {
    ..
  },
  ".some-class": {
    ..
  }
}
  • class_name must starts with dot ..

For text widgets

For node widgets

Interaction

All widgets provided by Famiq have Interaction component attached by default. That means all those widgets will emit FaInteractionEvent to bevy's EventReader with either Pressed, Hovered or None.

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

Available widget types

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

Handle interaction

You can write a bevy system to handle Famiq’s widgets interaction.

fn handle_button_press_system(mut events: EventReader<FaInteractionEvent>) {
    for e in events.read() {
        if e.widget == WidgetType::Image && e.interaction == Interaction::Hovered {
            // make sure this works only with widgets that have id provided
            if let Some(id) = e.widget_id.as_ref() {
                match id.as_str() {
                    "#image-one-id" => {
                        // do something
                    },
                    "#image-two-id" => {
                        // do something
                    }
                    _ => ()
                }
            }
        }
    }
}

Famiq Widgets

Famiq provides some default widgets that are likely required in any UI development.

Default widgets

FaContainer

🟒 Doesn't need container
🟒 Accepts child/children

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

Widget API

pub fn fa_container<'a>(builder: &'a mut FamiqWidgetBuilder) -> FaContainerBuilder<'a> {
    // ..
}

usage

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

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

Example

Texts without container

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

Example 1

Texts inside container

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();

Example 2

Styling

id and classes can be provided to container to be able to style it from json file.

fa_container(&mut builder)
    .id("#container")
    .children([boss, mom])
    .build();
{
  "#container": {
    "background_color": "yellow",
    "border_color": "yellow",
    "border_radius": "10px 10px 10px 10px"
  }
}

Example 3

FaButton

🟑 Needs container
🟑 Doesn't accept child/children

Colors

pub enum BtnColor {
    Default,
    Primary,
    PrimaryDark,
    Secondary,
    Success,
    SuccessDark,
    Danger,
    DangerDark,
    Warning,
    WarningDark,
    Info,
    InfoDark
}

Shapes

pub enum BtnShape {
    Default,
    Round,
    Rectangle
}

Sizes

pub enum BtnSize {
    Small,
    Normal,
    Large,
}

Widget API

pub fn fa_button<'a>(builder: &'a mut FamiqWidgetBuilder, text: &str) -> FaButtonBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used inside FaContainer widget.

Built-in classes

  • Color: is-primary, is-primary-dark, is-secondary, is-danger, is-danger-dark, is-info, is-info-dark, is-success, is-success-dark, is-warning, is-warning-dark.

  • Size: is-small, is-normal, is-large.

  • Shape: is-round, is-rectangle.

Example

// default
let my_btn = fa_button(&mut builder, "Press me")
    .id("#my-btn")
    .build();

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

// success & small
let small_btn = fa_button(&mut builder, "Press me")
    .class("is-success is-small")
    .build();

// warning & large
let large_btn = fa_button(&mut builder, "Press me")
    .class("is-warning is-large")
    .build();

fa_container(&mut builder)
    .children([my_btn, info_btn, small_btn, large_btn])
    .build();

Example 1

Handle button press

fn handle_button_press_system(mut events: EventReader<FaInteractionEvent>) {
    for e in events.read() {
        if e.is_button_pressed() {
            // make sure this works only with buttons that have id provided
            if let Some(id) = e.widget_id.as_ref() {
                match ud.as_str() {
                    "#my-btn" => {
                        // do something with my button
                    },
                    "#info-btn" => {
                        // do something with info button
                    }
                    _ => ()
                }
            }
        }
    }
}

FaText

🟑 Needs container
🟑 Doesn't accept child/children

API

pub fn fa_text<'a>(builder: &'a mut FamiqWidgetBuilder, value: &str) -> FaTextBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used inside FaContainer widget.

Example

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

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

Example 2

Resource

pub struct FaTextResource;
  • FaTextResource can be used to get & update specific text widget's value by either its id or entity.

    Available methods:

    • get_value_by_id: get fa_text value by id, return empty string if id doesn't exist.
    • get_value_by_entity: get fa_text value by entity, return empty string if entity doesn't exist.
    • update_value_by_id: update fa_text value by id.
    • update_value_by_entity: update fa_text value by entity.

    Example of using FaTextResource

    fn my_system(mut text_res: ResMut<FaTextResource>) {
        // some logic ..
    
        // get value
        let text = text_res.get_value_by_id("#boss-txt");
    
        // update value
        text_res.update_value_by_id("#boss-txt", "Good morning Boss");
    }

FaFpsText

🟒 Doesn't need container
🟑 Doesn't accept child/children

Widget API

pub fn fa_fps<'a>(builder: &'a mut FamiqWidgetBuilder) -> FaFpsTextBuilder<'a> {
    // ..
}

Usage

fa_fps(&mut builder).build();

return Entity which can be used as a child of a FaContainer.

  • change_color(): change number color based on the value.
  • right_side(): make fps text appears at the top right corner.

Example

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

FaTextInput

🟑 Needs container
🟑 Doesn't accept child/children

Variants

pub enum TextInputVariant {
    Default,
    Outlined,
    Underlined,
}

Colors

pub enum TextInputColor {
    Default,
    Primary,
    Secondary,
    Success,
    Danger,
    Warning,
    Info,
}

Sizes

pub enum TextInputSize {
    Small,
    Normal,
    Large,
}

Shapes

pub enum TextInputShape {
    Default,
    Round,
    Rectangle
}

Widget API

pub fn fa_text_input<'a>(
    builder: &'a mut FamiqWidgetBuilder,
    placeholder: &str
) -> FaTextInputBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used as child of FaContainer widget.

Built-in classes

  • Color: is-primary, is-secondary, is-warning, is-info, is-success, is-danger.

  • Size: is-small, is-normal, is-large.

  • Shapes: is-round, is-rectangle.

  • Variant: is-underlined, is-outlined.

Example

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

// info & large
let input_info_large = fa_text_input(&mut builder, "Enter your name")
    .class("is-info is-large")
    .build();

// warning & round
let input_warning_round = fa_text_input(&mut builder, "Enter your name")
    .class("is-warning is-round")
    .build();

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

Example 1

Resource

pub struct FaTextInputResource;
  • FaTextInputResource can be used to retrieve specific fa_text_input value by either id or entity.

    Available methods:

    • get_value_by_id: get input value by id, return empty string it id doesn't exist.
    • get_value_by_entity: get input value by entity, return empty string it entity doesn't exist.

    Example of using FaTextInputResource

    fn my_system(input_res: Res<FaTextInputResource>) {
        // some logic ..
    
        // get value
        let text = input_res.get_value_by_id("#name-input");
    }

FaSelection

🟑 Needs container
🟑 Doesn't accepts child/children

Variants

pub enum SelectorVariant {
    Outlined,
    Default,
    Underlined,
}

Colors

pub enum SelectorColor {
    Default,
    Primary,
    Secondary,
    Success,
    Danger,
    Warning,
    Info,
}

Sizes

pub enum SelectionSize {
    Small,
    Normal,
    Large,
}

Shapes

pub enum SelectorShape {
    Default,
    Round,
    Rectangle
}

Widget API

pub fn fa_selection<'a>(
    builder: &'a mut FamiqWidgetBuilder,
    placeholder: &str
) -> FaSelectionBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used as child of FaContainer widget.

Built-in classes

  • Color: is-primary, is-secondary, is-warning, is-info, is-success, is-danger.

  • Size: is-small, is-normal, is-large.

  • Shapes: is-round, is-rectangle.

  • Variant: is-underlined, is-outlined.

Example

let plans = fa_selection(&mut builder, "Select plan")
    .id("#plan")
    .class("is-info")
    .choices(["Personal", "Team", "Enterprise"])
    .build();
);

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

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

Example 1

Resource

pub struct FaSelectionResource;
  • FaSelectionResource can be used to retrieve specific fa_selection value by either id or entity.

    Available methods:

    • get_value_by_id: get input value by id, return empty string it id doesn't exist.
    • get_value_by_entity: get input value by entity, return empty string it entity doesn't exist.

    Example of using FaSelectionResource

    fn my_system(input_res: Res<FaSelectionResource>) {
        // some logic ..
    
        // get value
        let text = input_res.get_value_by_id("#plan");
    }

FaCircular

🟑 Needs container
🟑 Doesn't accept child/children

Spinning circular.

Variants

pub enum CircularColor {
    Default,
    Primary,
    Secondary,
    Success,
    Danger,
    Warning,
    Info,
}

Sizes

pub enum CircularSize {
    Small,
    Normal,
    Large
}

API

pub fn fa_circular<'a>(builder: &'a mut FamiqWidgetBuilder) -> FaCircularBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used inside FaContainer widget.

  • size(): set custom size of fa_circular.

Built-in classes

  • Color: is-primary, is-secondary, is-warning, is-info, is-success, is-danger.

  • Size: is-small, is-normal, is-large.

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 is-large")
    .build();

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

Example 1

FaModal

🟒 Doesn't need container
🟒 Accepts child/children

API

pub fn fa_modal<'a>(builder: &'a mut FamiqWidgetBuilder) -> FaModalBuilder<'a> {
    // ..
}

Usage

let modal = fa_modal(&mut builder).build();

Resource

pub struct FaModalState;
  • FaModalState can be used to show and hide specific modal by either id or entity.

    Available methods:

    • show_by_id: show modal by id.
    • show_by_entity: show modal by entity.
    • hide_by_id: hide modal by id.
    • hide_by_entity: hide modal by entity.

    Example of using FaModalState

    fn my_system(mut modal_state: ResMut<FaModalState>) {
        // some logic ..
    
        // show modal
        modal_state.show_by_id("#modal-id");
    
        // hide modal
        modal_state.hide_by_id("#modal-id");
    }

FaListView

🟒 Doesn't need container
🟒 Accepts child/children

API

pub fn fa_listview<'a>(builder: &'a mut FamiqWidgetBuilder) -> FaListViewBuilder<'a> {
    // ..
}

return Entity which can be used as child of FaContainer.

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();

FaImage

🟑 Needs container
🟑 Doesn't accept child/children

API

pub fn fa_image<'a>(builder: &'a mut FamiqWidgetBuilder, path: &str) -> FaImageBuilder<'a> {
    // ..
}
  • path: Path to image relative to assets folder.
  • Return entity of the widget which must be used as child of FaContainer widget.
  • Support only jpg and png format.

Usage

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

Custom size

By default, fa_image will load image at the original size. To use custom size, simply call size() method.

let image = fa_image(&mut builder, "path/to/image.jpg")
    .size(Val::Px(200.0), Val::Px(200.0))
    .build();

Example

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

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

Example 1

FaBgImage

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

🟒 Doesn't need container
🟑 Doesn't accept child/children

API

pub fn fa_bg_image<'a>(builder: &'a mut FamiqWidgetBuilder, path: &str) -> FaBgImageBuilder<'a> {
    // ..
}
  • path: Path to image relative to assets folder.
  • Support only jpg and png format.

Usage

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

Example

fa_bg_image(&mut builder, "logo.jpeg").build();

let btn = fa_button(&mut builder, "Press me").class("is-primary").build();

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

Example 1

FaProgressBar

🟑 Needs container
🟑 Doesn't accept child/children

Sizes

pub enum ProgressBarSize {
    Normal,
    Small,
    Large
}

Colors

pub enum ProgressBarColor {
    Default,
    Primary,
    PrimaryDark,
    Secondary,
    Success,
    SuccessDark,
    Danger,
    DangerDark,
    Warning,
    WarningDark,
    Info,
    InfoDark
}

Widget API

pub fn fa_progress_bar<'a>(
    builder: &'a mut FamiqWidgetBuilder
) -> FaProgressBarBuilder<'a> {
    // ..
}

Usage

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

Return Entity of the widget which must be used as child of FaContainer widget.

  • percentage(): by default, fa_progress_bar is indeterminate, use percentage() to set percentage.

Built-in classes

  • Color: is-primary, is-secondary, is-warning, is-info, is-success, is-danger.

  • Size: is-small, is-normal, is-large.

Example

// default
let default_bar = fa_progress_bar(&mut builder)
    .id("#default-bar")
    .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)
    .id("#warning-bar")
    .percentage(50.0)
    .class("is-warning")
    .build();

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

Resource

pub struct FaProgressBarResource;
  • FaProgressBarResouce can be used to set get & set percentage of the bars or set to indeterminate.

    Available methods:

    • get_percentage_by_id: get percentage by id, return None if id doesn't exist.
    • get_percentage_by_entity: get percentage by entity, return None if entity doesn't exist.
    • set_percentage_by_id: set percentage by id.
    • set_percentage_by_entity: set percentage by entity.

    Example of FaProgressBarResource

    fn my_system(bar_res: Res<FaProgressBarResource>) {
        // some logic ..
    
        // return None as #default-bar is indeterminate
        let default_bar_percent = bar.get_percentage_by_id("#default-bar");
    
        // return 50.0
        let warning_bar_percent = bar.get_percentage_by_id("#warning-bar").unwrap();
    
        // set to None to make bar indeterminate
        bar.set_percentage_by_id("#warning-bar", None);
    
        // set #default-bar to 30 percent
        bar.set_percentage_by_id("#default-bar", Some(30.0));
    }