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.
β οΈ 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 2dcamera
.new_no_camera()
: use this method if you want to spawn your owncamera
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
anddialog
. -
Non-containable widgets cannot have children and must be placed inside a containable widget. including
text
,button
,text_input
,selection
,image
,circular
andprogress_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.
-
color
: text color, supports onlysrgba
,linear_rgba
,hsla
and basic colors.Examples,
"color": "srgba 0.961, 0.0, 0.784, 0.9"
"color": "yellow"
https://docs.rs/bevy/latest/bevy/prelude/struct.TextColor.html
-
font_size
: text font size.Example,
"font_size": "14"
https://docs.rs/bevy/latest/bevy/prelude/struct.TextFont.html#structfield.font_size
-
background_color
: supports onlysrgba
,linear_rgba
,hsla
and basic colors.Examples,
"background_color": "srgba 0.961, 0.0, 0.784, 0.95"
"background_color": "green"
https://docs.rs/bevy/latest/bevy/prelude/struct.BackgroundColor.html
-
border_color
: supports onlysrgba
,linear_rgba
,hsla
and basic colors.Examples,
"border_color": "linear_rgba 0.961, 0.0, 0.784, 0.9"
"border_color": "pink"
https://docs.rs/bevy/latest/bevy/prelude/struct.BorderColor.html
-
border_radius
: top_left, top_right, bottom_left, bottom_right.Example,
"border_radius": "10px 10px 10px 10px"
https://docs.rs/bevy/latest/bevy/prelude/struct.BorderRadius.html
-
visibility
: supports onlyvisible
,hidden
andinherited
.https://docs.rs/bevy/latest/bevy/prelude/enum.Visibility.html
-
z_index
: indicates that a widgetβs front-to-back ordering is not controlled solely by its location in the UI hierarchy. A widget with a higher z-index will appear on top of sibling widgets with a lower z-index.Example,
"z_index": "2"
-
display
: defines the layout model used by node. Supportsflex
,grid
,block
andnone
. -
position_type
: the strategy used to position node. Supportsrelative
andabsolute
.https://docs.rs/bevy/latest/bevy/prelude/enum.PositionType.html
-
overflow_x
: whether to show or clip overflowing items on the x axis. Supportsvisible
,clip
,hidden
andscroll
.https://docs.rs/bevy/latest/bevy/prelude/struct.Overflow.html
-
overflow_y
: whether to show or clip overflowing items on the y axis. Supportsvisible
,clip
,hidden
andscroll
.https://docs.rs/bevy/latest/bevy/prelude/struct.Overflow.html
-
left
: the horizontal position of the left edge of the widget.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.left
-
right
: the horizontal position of the right edge of the widget.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.right
-
top
: the vertical position of the top edge of the widget.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.top
-
bottom
: the vertical position of the bottom edge of the widget.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.bottom
-
width
: the ideal width of the widget. width is used when it is within the bounds defined bymin_width
andmax_width
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.width
-
height
: the ideal height of the widget. height is used when it is within the bounds defined bymin_height
andmax_height
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.height
-
min_width
: the minimumwidth
of the widget.min_width
is used if it is greater thanwidth
and/ormax_width
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.min_width
-
min_height
: the minimumheight
of the widget.min_height
is used if it is greater thanheight
and/ormax_height
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.min_height
-
max_width
: the maximumwidth
of the widget.max_width
is used if it is within the bounds defined bymin_width
andwidth
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.max_width
-
max_height
: the maximumheight
of the widget.max_height
is used if it is within the bounds defined bymin_height
andheight
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.max_height
-
align_items
: used to control how each individual item is aligned by default within the space theyβre given. Supportsdefault
,start
,end
,flex_start
,flex_end
,center
,base_line
andstretch
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.align_items
-
justify_items
: used to control how each individual item is aligned by default within the space theyβre given. Supportsdefault
,start
,end
,flex_start
,flex_end
,center
,base_line
andstretch
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.justify_items
-
align_left
: used to control how the specified item is aligned within the space itβs given. Supportsauto
,start
,end
,flex_start
,flex_end
,center
,base_line
andstretch
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.align_self
-
justify_content
: used to control how items are distributed. Supportsdefault
,start
,end
,flex_start
,flex_end
,center
,stretch
,space_between
,space_evenly
andspace_around
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.justify_content
-
margin
: left, right, top, bottom.Example,
"margin": "10px 10px 5px 5px"
https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.margin
-
margin_left
: this will override left value defined inmargin
. -
margin_right
: this will override right value defined inmargin
. -
margin_top
: this will override top value defined inmargin
. -
margin_bottom
: this will override bottom value defined inmargin
. -
padding
: left, right, top, bottom.Example,
"padding": "10px 10px 5px 5px"
https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.padding
-
padding_left
: this will override left value defined inpadding
. -
padding_right
: this will override right value defined inpadding
. -
padding_top
: this will override top value defined inpadding
. -
padding_bottom
: this will override bottom value defined inpadding
. -
border
: left, right, top, bottom.Example,
"padding": "10px 10px 5px 5px"
https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.border
-
border_left
: this will override left value defined inborder
. -
border_right
: this will override right value defined inborder
. -
border_top
: this will override top value defined inborder
. -
border_bottom
: this will override bottom value defined inborder
. -
flex_direction
: whether a Flexbox container should be a row or a column. Supportsrow
,column
,row_reverse
andcolumn_reverse
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.flex_direction
-
flex_wrap
: whether a Flexbox container should wrap its contents onto multiple lines if they overflow. Supportsno_wrap
,wrap
andwrap_reverse
.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.flex_wrap
-
flex_grow
: defines how much a flexbox item should grow if thereβs space available. Defaults to "0" (donβt grow at all).https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.flex_grow
-
flex_shrink
: defines how much a flexbox item should shrink if thereβs not enough space available. Defaults to 1.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.flex_shrink
-
flex_basis
: the initial length of a flexbox in the main axis, before flex growing/shrinking properties are applied.https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html#structfield.flex_basis
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 atx
*5px
.mt-auto
|mb-auto
|ml-auto
|mr-auto
: margin auto for top, bottom, left and right.my-x
: margin top and bottom atx
*5px
.my-auto
: margin auto for top and bottom.mx-x
: margin left and right atx
*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 atx
*5px
.pt-auto
|pb-auto
|pl-auto
|pr-auto
: padding auto for top, bottom, left and right.py-x
: padding top and bottom atx
*5px
.py-auto
: padding auto for top and bottom.px-x
: padding left and right atx
*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 toauto
, 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 asindeterminate
. 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.
- 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;
}
- 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.