Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Build high performance GUI app with power of Game Engine (Bevy).

What is Bevy?

Bevy is a game engine written in Rust, based on ECS architecture. It’s capable of rendering 2D and 3D graphics. Bevy also provides full UI system to build GPU accelerated GUI application but because it’s based on ECS, building UI with Bevy is verbosity.

Let’s see an example below of creating a simple button with pure Bevy’s UI.

#![allow(unused)]
fn main() {
fn setup(mut commands: Commands) {
    commands.spawn((
        Node {
            width: px(100),
            height: px(20),
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
            padding: UiRect::all(px(10)),
            ..default()
        },
        BorderRadius::all(px(8)),
        BackgroundColor(Color::srgb(1.0, 1.0, 1.0)),
        
        children![
            (
                Text::new("Click me"),
                TextColor(Color::BLACK)
            )
        ],
    
        observe(on_button_click)
    ))
}

fn on_button_click(_click: On<Pointer<Click>>) {
    // do something
}
}

As you can see, it’s really easy to get lost with Bevy’s UI. A real GUI application will have more than just a button and complex hierarchy.

What is Makara?

Makara is a UI Library built on top of Bevy engine. It simplifies the verbosity by providing built-in widgets and high level api with builder pattern that most rust devs familiar with, to make it easy to write and maintain the code.

Let’s see how Makara reduce the verbosity.

#![allow(unused)]
fn main() {
fn setup(mut commands: Commands) {
    commands.spawn(
        button_!(
            "Click me", 
            background_color: "white", 
            padding: px(10),
            on: on_button_click
        )
    );
}

fn on_button_click(_clicked: On<Clicked>) {
    // do something
}
}

Now it’s much cleaner compare to previous example.

Another motivation behind this project is the performance inherent to a modern game engine. Bevy utilizes a decoupled architecture consisting of a Spatial Simulation and an Interface Overlay. This allows for hardware-accelerated rendering of complex 2D/3D meshes and custom shaders within the World Space, while maintaining a highly responsive, independent UI.

Bevy Layers

That means you can render any kind of heavy graphic within your application without sacrifying performance.

Core Features

  • Built-in widgets including button, modal, text input and more.
  • Leverages Bevy’s massive parallelism for smooth and efficient rendering.
  • Routing systems.
  • Custom styling with ID & Classes similar to HTML/CSS.
  • High level API and flexible.

Installation

cargo add makara

Getting Start

IMPORTANT

Makara relies entire on Bevy’s UI system and ECS. When you use Makara, you’re actually using the Bevy engine, with Makara providing an abstraction for its UI system to help you build GUI applications with ease.

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 makara::prelude::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(
        root_!(
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
            
            [ text_!("Hello world", font_size: 20.0) ]
        )
    );
}

An overview of ECS

Because this library is powered by a game engine which is based on ECS architecture, it’s a must to understand how ECS works.

ECS stands for Entity-Component-System.

  • Entity: entities are objects you see when you run your application. A button widget is an entity, a text widget is an entity, the list goes on. Each entity consists of one or multiple components.

  • Component: think of component as a tag. It’s used to identify different kind of data. In the context of Makara or Bevy, component is just a rust struct or enum.

    For example, you create a button with background color of red and border color of blue in Makara.

    #![allow(unused)]
    fn main() {
    // create button with background color of red and border color of blue 
    button_!("Click me", background_color: "red", border_color: "blue");
    }

    Under the hood, the button is just an entity that contains these 2 components, BackgroundColor and BorderColor which looks something like this.

    #![allow(unused)]
    fn main() {
    (
        BackgroundColor(Color::srgb(1.0, 0.0, 0.0)),
        BorderColor(Color::srgb(0.0, 1.0, 0.0)),
        // other components
    )
    }

    It’s then up to the Bevy engine to translate it to shading language and render it on your GPU.

  • System: systems are just rust functions that take specific arguments and run at specific schedule. Startup schedule means the functions run only once at the beginning. Update schedule means the functions run every single frame. And of course, there are more schedules than just that.

    fn main() {
      App::new()
          .add_plugins(MakaraPlugin::default())
          .add_systems(Startup, setup)
          .run();
    }
    
    fn setup(mut commands: Commands) {
        // do something
    }

    Here, setup function is a system that take a special argument, in this case Commands. It’s being registered to run at Startup schedule which means it only do something once and that’s it.

Understand Makara hierarchy

Just like any other UI frameworks, you need a starting point for your application. In Makara, you start by spawning a single root widget and add other widgets as children of root except modal.

In above example, when you run the application with cargo run you will see the text “Hello world” appears at the center of the screen.

You can make a widget becomes a child of another widget by using [], we will talk about it later.

Widget Structure

Understanding the structure of widgets in Makara

In Makara, creating a widget can be done by calling its pre-defined macro. A button can be created using button_! and a text can be created using text_!.

Each widget macro is splitted into 3 parts:

  1. Properties
  2. Event handlers
  3. Children array

For example,

#![allow(unused)]
fn main() {
scroll_!( 
    // properties
    background_color: "red",
    border_radius: px(5),
    margin: px(5),
    padding: px(10),
    
    // event handlers (if there's any)
    on: |_scrolling: On<Scrolling>| {},
    on: |_built: On<WidgetBuilt>| {},
    
    // children array
    [
        button_!("Click me", background_color: "blue"),
        text_!("This is some text")
    ]
);
}

For event handlers, you can also create functions for it to make it cleaner.

#![allow(unused)]
fn main() {
scroll_!( 
    // properties
    background_color: "red",
    border_radius: px(5),
    margin: px(5),
    padding: px(10),
    
    // event handlers (if there's any)
    on: handle_scrolling,
    on: handle_widget_built,
    
    // children array
    [
        button_!("Click me", background_color: "blue"),
        text_!("This is some text")
    ]
);

fn handle_scrolling(_scrolling: On<Scrolling>) {
    // do something
}

fn handle_widget_built(_built: On<WidgetBuilt>) {
    // do something
}
}

Widgets

Makara provides a comprehensive set of built-in widgets for building modern GUI applications. Each widget comes with styling options, event handling capabilities, and built-in class support for rapid development.

Container Widgets

Container widgets manage layout and can contain child elements.

Root

The root_! widget is the starting point for your UI hierarchy and must be spawned by commands. It supports alignment utility classes.

Basic Usage:

#![allow(unused)]
fn main() {
commands.spawn(root_!([
    text_!("Hello World")
]));
}

With styling and alignment:

#![allow(unused)]
fn main() {
commands.spawn(
    root_!(
        background_color: Color::srgb(0.1, 0.1, 0.2),
        class: "justify-center align-center p-4",
        
        [
            column_!([
                text_!("Welcome to Makara!"),
                button_!("Get Started")
            ])
        ]
    )
);
}

With custom properties:

#![allow(unused)]
fn main() {
commands.spawn(
    root_!(
        width: percent(100),
        height: percent(100),
        id: "main-container",
        class: "justify-between align-stretch",
        
        [/* children */]
    )
);
}

Column

The column_! widget arranges children vertically in a flexbox column layout.

Basic Usage:

#![allow(unused)]
fn main() {
column_!([
    text_!("Item 1"),
    text_!("Item 2"),
    text_!("Item 3")
])
}

With alignment and spacing:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-center align-center p-4 m-2",
    background_color: "white",
    
    [
        text_!("Centered Column", font_size: 24.0),
        button_!("Action Button", class: "is-primary mt-3"),
        text_!("Footer text", class: "mt-2")
    ]
)
}

With event handling:

#![allow(unused)]
fn main() {
column_!(
    on: |_built: On<WidgetBuilt>| {
        println!("Column widget built!");
    },
    
    [/* children */]
)
}

Row

The row_! widget arranges children horizontally in a flexbox row layout.

Basic Usage:

#![allow(unused)]
fn main() {
row_!([
    text_!("Left"),
    text_!("Center"), 
    text_!("Right")
])
}

Navigation bar example:

#![allow(unused)]
fn main() {
row_!(
    class: "justify-between align-center p-3",
    background_color: Color::srgb(0.2, 0.3, 0.8),
    
    [
        text_!("MyApp", font_size: 20.0, color: Color::WHITE),
        row_!(
            class: "justify-end align-center",
            
            [
                button_!("Home", class: "mr-2"),
                button_!("About", class: "mr-2"),
                button_!("Contact")
            ]
        )
    ]
)
}

Scroll

The scroll_! widget creates a scrollable container for content that exceeds the container size.

Basic Usage:

#![allow(unused)]
fn main() {
scroll_!([
    text_!("Line 1"),
    text_!("Line 2"),
    text_!("Line 3"),
    // ... many more lines
])
}

Styled scroll container:

#![allow(unused)]
fn main() {
scroll_!(
    width: px(300),
    height: px(200),
    class: "p-3",
    background_color: Color::srgb(0.95, 0.95, 0.95),
    
    [
        column_!(
            class: "justify-start align-stretch",
            
            [
                text_!("Scrollable Content", font_size: 18.0, class: "mb-2"),
                text_!("This is line 1"),
                text_!("This is line 2"),
                // ... more content
            ]
        )
    ]
)
}

With scroll event handling:

#![allow(unused)]
fn main() {
scroll_!(
    on: |scrolling: On<Scrolling>| {
        println!("User is scrolling!");
    },
    
    [/* scrollable content */]
)
}

Interactive Widgets

Button

The button_! widget provides clickable interactions with built-in hover states.

Basic Usage:

#![allow(unused)]
fn main() {
button_!("Click Me")
}

With styling:

#![allow(unused)]
fn main() {
button_!("Submit", class: "is-primary p-3 m-2")
}

With event handling:

#![allow(unused)]
fn main() {
button_!(
    "Save Data";
    on: |clicked: On<Clicked>| {
        println!("Save button clicked!");
    },
    on: |over: On<MouseOver>| {
        println!("Mouse over save button");
    },
    on: |out: On<MouseOut>| {
        println!("Mouse left save button");  
    }
)
}

Complete example with styling and events:

#![allow(unused)]
fn main() {
button_!(
    "Delete Item",
    id: "delete-btn",
    class: "is-danger p-2 m-1",
    on: |clicked: On<Clicked>| {
        // Handle delete action
    },
    on: |built: On<WidgetBuilt>| {
        println!("Delete button ready!");
    }
)
}

Text Input

The text_input_! widget provides text entry with placeholder support and theming.

Basic Usage:

#![allow(unused)]
fn main() {
text_input_!("Enter your name...")
}

With styling and validation:

#![allow(unused)]
fn main() {
text_input_!(
    "Enter email address...",
    class: "is-primary p-2",
    width: px(300),
    id: "email-input"
)
}

With change event handling:

#![allow(unused)]
fn main() {
text_input_!(
    "Search...";
    on: |change: On<Change<String>>| {
        println!("Search text: {}", change.data);
    };
    on: |clicked: On<Clicked>| {
        println!("Input field focused");
    }
)
}

Form example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-stretch p-4";
    [
        text_!("User Registration", font_size: 20.0, class: "mb-3"),
        text_input_!("Full Name...", class: "mb-2", width: px(300)),
        text_input_!("Email Address...", class: "is-info mb-2", width: px(300)),
        text_input_!("Password...", class: "is-warning mb-3", width: px(300)),
        button_!("Register", class: "is-success")
    ]
)
}

Checkbox

The checkbox_! widget provides boolean toggle functionality.

Basic Usage:

#![allow(unused)]
fn main() {
checkbox_!("Accept terms and conditions")
}

With styling:

#![allow(unused)]
fn main() {
checkbox_!("Enable notifications", class: "is-primary p-2")
}

With state change handling:

#![allow(unused)]
fn main() {
checkbox_!(
    "Subscribe to newsletter";
    on: |active: On<Active<String>>| {
        println!("Checkbox activated: {}", active.data);
    };
    on: |inactive: On<Inactive<String>>| {
        println!("Checkbox deactivated: {}", inactive.data);
    }
)
}

Settings panel example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-start p-4";
    [
        text_!("Settings", font_size: 18.0, class: "mb-3"),
        checkbox_!("Dark mode", class: "is-info mb-2"),
        checkbox_!("Auto-save", class: "is-success mb-2"),  
        checkbox_!("Show tooltips", class: "is-primary mb-2"),
        button_!("Apply Settings", class: "is-primary mt-2")
    ]
)
}

Radio Group & Radio

Radio widgets provide single-selection from multiple options.

Basic Usage:

#![allow(unused)]
fn main() {
radio_group_!([
    radio_!("Option A"),
    radio_!("Option B"),
    radio_!("Option C")
])
}

Payment method selector:

#![allow(unused)]
fn main() {
radio_group_!(
    class: "p-3";
    on: |change: On<Change<String>>| {
        println!("Selected payment method: {}", change.data);
    };
    [
        radio_!("Credit Card", class: "is-primary mb-2"),
        radio_!("PayPal", class: "is-info mb-2"),
        radio_!("Bank Transfer", class: "is-success mb-2"),
        radio_!("Cash on Delivery", class: "is-warning")
    ]
)
}

With individual radio event handling:

#![allow(unused)]
fn main() {
radio_group_!([
    radio_!(
        "Small Size";
        on: |active: On<Active>| {
            println!("Small size selected");
        }
    ),
    radio_!("Medium Size"),
    radio_!("Large Size")
])
}

Select

The select_! widget provides a dropdown selection interface.

Basic Usage:

#![allow(unused)]
fn main() {
select_!("Choose country...", choices: &["USA", "Canada", "Mexico"])
}

With styling and events:

#![allow(unused)]
fn main() {
select_!(
    "Select payment method...",
    choices: &["Credit Card", "Debit Card", "PayPal"],
    class: "is-primary p-2",
    width: px(250);
    on: |change: On<Change<String>>| {
        println!("Selected: {}", change.data);
    };
    on: |active: On<Active>| {
        println!("Dropdown opened");
    };
    on: |inactive: On<Inactive>| {
        println!("Dropdown closed");
    }
)
}

Form integration:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-stretch p-4";
    [
        text_!("Shipping Information"),
        select_!("Country...", choices: &["USA", "Canada", "UK"], class: "mb-2"),
        select_!("State/Province...", choices: &["CA", "NY", "TX"], class: "mb-2"),
        button_!("Continue", class: "is-success")
    ]
)
}

The dropdown_! widget creates a button that shows/hides child content.

Basic Usage:

#![allow(unused)]
fn main() {
dropdown_!(
    "Menu";
    [
        button_!("Profile"),
        button_!("Settings"),
        button_!("Logout")
    ]
)
}

Navigation dropdown:

#![allow(unused)]
fn main() {
dropdown_!(
    "Products",
    class: "is-primary";
    on: |clicked: On<Clicked>| {
        println!("Products dropdown toggled");
    };
    [
        button_!("Laptops", class: "mb-1"),
        button_!("Phones", class: "mb-1"),
        button_!("Tablets", class: "mb-1"),
        button_!("Accessories")
    ]
)
}

Slider

The slider_! widget provides numeric input via draggable control.

Basic Usage:

#![allow(unused)]
fn main() {
slider_!(min: 0.0, max: 100.0)
}

Volume control example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-center p-4";
    [
        text_!("Volume Control", class: "mb-2"),
        slider_!(
            min: 0.0, 
            max: 100.0,
            class: "is-primary p-2";
            on: |change: On<Change<f32>>| {
                println!("Volume: {:.1}%", change.data);
            }
        )
    ]
)
}

Settings panel with multiple sliders:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-stretch p-4";
    [
        text_!("Audio Settings", font_size: 18.0, class: "mb-3"),
        
        column_!(class: "mb-2"; [
            text_!("Master Volume"),
            slider_!(min: 0.0, max: 100.0, class: "is-primary")
        ]),
        
        column_!(class: "mb-2"; [
            text_!("Music Volume"),
            slider_!(min: 0.0, max: 100.0, class: "is-info")
        ]),
        
        column_!(class: "mb-2"; [
            text_!("Effects Volume"), 
            slider_!(min: 0.0, max: 100.0, class: "is-success")
        ])
    ]
)
}

Display Widgets

Text

The text_! widget displays styled text content.

Basic Usage:

#![allow(unused)]
fn main() {
text_!("Hello World")
}

With styling:

#![allow(unused)]
fn main() {
text_!("Welcome!", font_size: 24.0, color: Color::srgb(0.2, 0.6, 0.8))
}

With classes:

#![allow(unused)]
fn main() {
text_!("Error message", class: "is-danger", font_size: 16.0)
}

Typography example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-start align-start p-4";
    [
        text_!("Main Heading", font_size: 32.0, class: "mb-3"),
        text_!("Subheading", font_size: 20.0, class: "is-info mb-2"),
        text_!("Regular paragraph text with some content.", font_size: 14.0, class: "mb-2"),
        text_!("Small caption text", font_size: 12.0, class: "is-secondary")
    ]
)
}

Image

The image_! widget displays images from file paths or URLs.

Basic Usage:

#![allow(unused)]
fn main() {
image_!("assets/logo.png")
}

With sizing:

#![allow(unused)]
fn main() {
image_!(
    "assets/hero-banner.jpg",
    width: px(400),
    height: px(200)
)
}

With events:

#![allow(unused)]
fn main() {
image_!(
    "assets/profile.png";
    on: |clicked: On<Clicked>| {
        println!("Profile image clicked");
    };
    on: |over: On<MouseOver>| {
        println!("Hovering over image");
    }
)
}

Image gallery example:

#![allow(unused)]
fn main() {
row_!(
    class: "justify-around align-center p-4";
    [
        image_!("assets/thumb1.jpg", width: px(150), height: px(150)),
        image_!("assets/thumb2.jpg", width: px(150), height: px(150)),
        image_!("assets/thumb3.jpg", width: px(150), height: px(150))
    ]
)
}

The link_! widget creates clickable links that open in the browser.

Basic Usage:

#![allow(unused)]
fn main() {
link_!("https://rust-lang.org/")
}

With custom text:

#![allow(unused)]
fn main() {
link_!("Visit Rust Website", url: "https://rust-lang.org/")
}

With styling and events:

#![allow(unused)]
fn main() {
link_!(
    "Documentation", 
    url: "https://docs.rs/makara",
    class: "is-primary";
    on: |clicked: On<Clicked>| {
        println!("Documentation link clicked");
    }
)
}

Footer links example:

#![allow(unused)]
fn main() {
row_!(
    class: "justify-center align-center p-3";
    [
        link_!("Privacy Policy", url: "https://example.com/privacy", class: "mr-4"),
        link_!("Terms of Service", url: "https://example.com/terms", class: "mr-4"),
        link_!("Contact Us", url: "https://example.com/contact")
    ]
)
}

Progress & Loading Widgets

Progress Bar

The progress_bar_! widget shows linear progress indication.

Basic Usage:

#![allow(unused)]
fn main() {
progress_bar_!()
}

With styling:

#![allow(unused)]
fn main() {
progress_bar_!(
    class: "is-primary p-2",
    width: px(300)
)
}

With progress tracking:

#![allow(unused)]
fn main() {
progress_bar_!(
    class: "is-success";
    on: |change: On<Change<f32>>| {
        println!("Progress: {:.1}%", change.data * 100.0);
    }
)
}

Loading interface example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-center align-center p-4";
    [
        text_!("Loading...", class: "mb-2"),
        progress_bar_!(class: "is-info", width: px(250)),
        text_!("Please wait while we process your request", 
               font_size: 12.0, class: "mt-2")
    ]
)
}

Circular

The circular_! widget shows circular/spinner progress indication.

Basic Usage:

#![allow(unused)]
fn main() {
circular_!()
}

With styling:

#![allow(unused)]
fn main() {
circular_!(
    class: "is-primary p-3",
    width: px(50),
    height: px(50)
)
}

Loading spinner example:

#![allow(unused)]
fn main() {
column_!(
    class: "justify-center align-center p-4";
    [
        circular_!(class: "is-info mb-3"),
        text_!("Processing...", class: "is-info")
    ]
)
}

The modal_! widget creates overlay dialogs and is independent of the root widget hierarchy.

Basic Usage:

#![allow(unused)]
fn main() {
commands.spawn(
    modal_!([
        column_!([
            text_!("Are you sure?"),
            button_!("Confirm")
        ])
    ])
);
}

Confirmation dialog:

#![allow(unused)]
fn main() {
modal_!(
    on: |active: On<Active>| {
        println!("Modal opened");
    };
    on: |inactive: On<Inactive>| {
        println!("Modal closed"); 
    };
    [
        column_!(
            class: "justify-center align-center p-4",
            background_color: Color::WHITE,
            width: px(300),
            height: px(200);
            [
                text_!("Delete Confirmation", font_size: 18.0, class: "mb-3"),
                text_!("Are you sure you want to delete this item?", class: "mb-4"),
                row_!(
                    class: "justify-center align-center";
                    [
                        button_!("Cancel", class: "is-secondary mr-2"),
                        button_!("Delete", class: "is-danger")
                    ]
                )
            ]
        )
    ]
)
}

Settings modal:

#![allow(unused)]
fn main() {
modal_!([
    column_!(
        class: "justify-start align-stretch p-4",
        background_color: Color::WHITE,
        width: px(400),
        height: px(500);
        [
            text_!("Settings", font_size: 24.0, class: "mb-4"),
            
            checkbox_!("Enable notifications", class: "mb-2"),
            checkbox_!("Auto-save documents", class: "mb-2"),
            
            text_!("Theme", class: "mb-1"),
            select_!("Choose theme...", choices: &["Light", "Dark", "Auto"], class: "mb-4"),
            
            row_!(
                class: "justify-end align-center";
                [
                    button_!("Cancel", class: "mr-2"),
                    button_!("Save", class: "is-primary")
                ]
            )
        ]
    )
])
}

Built-in Classes

All widgets support built-in utility classes:

Color Classes

  • is-primary, is-primary-dark
  • is-link, is-link-dark
  • is-info, is-info-dark
  • is-success, is-success-dark
  • is-warning, is-warning-dark
  • is-danger, is-danger-dark

Spacing Classes (Container widgets only)

  • Margin: m-0 to m-6, mt-2, mr-3, mb-1, ml-4, mx-2, my-3
  • Padding: p-0 to p-6, pt-2, pr-3, pb-1, pl-4, px-2, py-3

Alignment Classes (Container widgets only)

  • Justify: justify-start, justify-center, justify-end, justify-between, justify-around, justify-evenly
  • Align: align-start, align-center, align-end, align-stretch, align-baseline

Complete Example

Here’s a complete application example using multiple widgets:

#![allow(unused)]
fn main() {
use makara::prelude::*;
use bevy::prelude::*;

fn setup(mut commands: Commands) {
    commands.spawn(
        root_!(
            class: "justify-center align-center p-4",
            background_color: Color::srgb(0.9, 0.9, 0.9);
            [
                column_!(
                    class: "justify-start align-stretch p-4",
                    background_color: Color::WHITE,
                    width: px(500);
                    [
                        // Header
                        text_!("User Profile", font_size: 24.0, class: "mb-4"),
                        
                        // Profile image
                        row_!(
                            class: "justify-center align-center mb-4";
                            [
                                image_!("assets/avatar.png", width: px(100), height: px(100))
                            ]
                        ),
                        
                        // Form fields
                        text_input_!("Full Name...", class: "mb-2"),
                        text_input_!("Email...", class: "is-info mb-2"),
                        
                        // Preferences
                        text_!("Preferences", font_size: 16.0, class: "mb-2"),
                        checkbox_!("Email notifications", class: "mb-1"),
                        checkbox_!("Dark mode", class: "mb-3"),
                        
                        // Theme selection
                        select_!("Theme...", choices: &["Light", "Dark", "Auto"], class: "mb-4"),
                        
                        // Actions
                        row_!(
                            class: "justify-end align-center";
                            [
                                button_!("Cancel", class: "mr-2"),
                                button_!("Save Profile", class: "is-primary")
                            ]
                        )
                    ]
                )
            ]
        )
    );
}
}

For detailed API documentation, visit the Rust docs.

Querying Widgets

Each widget type has its own query and blueprint. Once widgets are built, you can query any widgets within systems using its corresponding query struct.

For example, let’s say we have a text and a button. Once the button is clicked, we want to update the text.

#![allow(unused)]
fn main() {
column_!([
    text_!("Hello world", id: "my-text"),
    button_!("Click me"; on: |_clicked: On<Clicked>, mut text_q: TextQuery| {
        if let Some(text) = text_q.find_by_id("my-text") {
            text.text.value.0 = "Hello Mom".to_string();
        }
    })
]);
}

In the example above, once the button is clicked, we query text widgets using TextQuery. We then find the specific widget using its id. This is similar to how javascript finds HTML elements within the DOM.

Of course, you can query a widget using class or entity instead of id. Querying with class is useful if you want to fetch multiple widgets at same time, but requires you to write extra code.

#![allow(unused)]
fn main() {
// change all text colors when button is clicked.
column_!([
    text_!("Hello world", class: "my-text"),
    text_!("Hello mom", class: "my-text"),
    text_!("Hello friend", class: "my-text"),
    text_!("Hello stranger", class: "my-text"),
    
    button_!("Click me"; on: |_clicked: On<Clicked>, mut text_q: TextQuery| {
        // find_by_class returns list of entity
        let text_entities = text_q.find_by_class("my-text");
    
        // iterate thru the list
        for entity in text_entities.into_iter() {
            if let Some(text) = text_q.find_by_entity(entity) {
                text.text.color.0 = Color::RED;
            }
        }
    })
]);
}

For more information about widget query and its blueprint, see widgets.

Styling

Styling in Makara

There are 2 ways to provide custom styles to widgets.

  1. Style on widgets directly using properties.
  2. Attach an ID or Classes and provide styles via ID or Classes.

Direct styling

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(
        root_!(
            align_items: AlignItems::Center, // direct styling
            justify_content: JustifyContent::Center; // direct styling
            
            [ text_!("Hello world", font_size: 20.0) ]
        )
    );
}

With ID and Classes

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, (setup, setup_styles)) // add this
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(
        root_!(id: "root"; [
            text_!("Hello world", id: "text-one", class: "text"),
            text_!("Hello mom", class: "text"),
            text_!("Hello friend", class: "text hello-friend"),
            text_!("Hello stranger", class: "text")
        ])
    );
}

fn setup_styles(mut style: ResMut<CustomStyle>) {
    // via id
    style.bind_id(
        "root",
        Style::new()
            .align_items(AlignItems::Center)
            .justify_content(JustifyContent::Center)
    );
    
    // via id
    style.bind_id("text-one", Style::new().color("red"));
    
    // via class
    style.bind_class(
        "text",
        Style::new().font_size(20.0)
    );
    
    // via class
    style.bind_class(
        "hello-friend",
        Style::new().color("blue")
    );
}

When you run the application:

  • root will get AlignItems and JustifyContent styles.
  • all texts will have font_size of 20.0.
  • only one text with id text-one will have color of red.
  • only one text with class hello-friend will have color of blue.

See Style API for more info.

Alignment Utility Classes

Makara provides a comprehensive set of alignment utility classes that allow you to control the layout and positioning of widgets without writing custom styles. These classes follow CSS flexbox conventions and work similarly to utility frameworks like Tailwind CSS.

Justify Content Classes

Justify content classes control the alignment of child elements along the main axis (horizontal for rows, vertical for columns).

Class NameAlternativeEffect
justify-startjustify-content-startAlign children to the start of the main axis
justify-centerjustify-content-centerCenter children along the main axis
justify-endjustify-content-endAlign children to the end of the main axis
justify-betweenjustify-content-betweenDistribute children with equal space between them
justify-aroundjustify-content-aroundDistribute children with equal space around them
justify-evenlyjustify-content-evenlyDistribute children with equal space around and between them
justify-stretchjustify-content-stretchStretch children to fill the main axis

Align Items Classes

Align items classes control the alignment of child elements along the cross axis (vertical for rows, horizontal for columns).

Class NameAlternativeEffect
align-startalign-items-startAlign children to the start of the cross axis
align-centeralign-items-centerCenter children along the cross axis
align-endalign-items-endAlign children to the end of the cross axis
align-stretchalign-items-stretchStretch children to fill the cross axis
align-baselinealign-items-baselineAlign children along their baseline

Align Content Classes

Align content classes control the alignment of wrapped lines when there are multiple rows/columns of content.

Class NameEffect
align-content-startAlign wrapped lines to the start
align-content-centerCenter wrapped lines
align-content-endAlign wrapped lines to the end
align-content-stretchStretch wrapped lines to fill the container
align-content-betweenDistribute wrapped lines with equal space between them
align-content-aroundDistribute wrapped lines with equal space around them
align-content-evenlyDistribute wrapped lines with equal space around and between them

Usage Examples

Basic Centering

#![allow(unused)]
fn main() {
// Center content both horizontally and vertically
root_!(
    class: "justify-center align-center";
    [ text_!("Centered content") ]
)
}

Button Layout

#![allow(unused)]
fn main() {
// Distribute buttons evenly across a row
row_!(
    class: "justify-between align-center";
    [
        button_!("Cancel"),
        button_!("Submit")
    ]
)
}

Card Layout

#![allow(unused)]
fn main() {
// Create a vertically centered card with content aligned to start
column_!(
    class: "justify-center align-start p-4";
    [
        text_!("Card Title", font_size: 18.0),
        text_!("Card content goes here..."),
        button_!("Action", class: "mt-3")
    ]
)
}
#![allow(unused)]
fn main() {
// Create a navigation bar with logo on left, menu on right
row_!(
    class: "justify-between align-center p-3";
    [
        text_!("Logo", font_size: 20.0),
        row_!(
            class: "justify-end align-center";
            [
                button_!("Home", class: "mr-2"),
                button_!("About", class: "mr-2"),
                button_!("Contact")
            ]
        )
    ]
)
}

Grid-like Layout

#![allow(unused)]
fn main() {
// Create a grid of items with even spacing
column_!(
    class: "justify-start align-stretch";
    [
        row_!(
            class: "justify-around align-center mb-4";
            [
                button_!("Item 1"),
                button_!("Item 2"),
                button_!("Item 3")
            ]
        ),
        row_!(
            class: "justify-around align-center";
            [
                button_!("Item 4"),
                button_!("Item 5"),
                button_!("Item 6")
            ]
        )
    ]
)
}

Combining with Other Classes

Alignment classes work seamlessly with other built-in utility classes:

#![allow(unused)]
fn main() {
// Combine alignment with spacing and color classes
column_!(
    class: "justify-center align-center p-4 m-2";
    background_color: Color::WHITE;
    [
        text_!("Welcome!", class: "mb-3", font_size: 24.0),
        button_!("Get Started", class: "is-primary")
    ]
)
}

Responsive Considerations

While Makara doesn’t have built-in responsive breakpoints like web CSS frameworks, you can dynamically change classes based on window size or other conditions in your Bevy systems:

#![allow(unused)]
fn main() {
fn adjust_layout_system(
    mut commands: Commands,
    windows: Query<&Window>,
    mut widgets: Query<(Entity, &mut Class), With<SomeWidget>>
) {
    if let Ok(window) = windows.get_single() {
        for (entity, mut class) in widgets.iter_mut() {
            if window.width() < 600.0 {
                // Mobile layout
                class.set("justify-center align-center".to_string());
            } else {
                // Desktop layout
                class.set("justify-between align-center".to_string());
            }
        }
    }
}
}

Supported Widgets

Alignment utility classes are supported on container widgets that manage child element layout:

  • root_!() - Main application container
  • row_!() - Horizontal flex container
  • column_!() - Vertical flex container
  • scroll_!() - Scrollable container

Other widgets like button_!(), text_!(), image_!(), etc. have fixed internal layouts and don’t support alignment classes since they’re not intended to be containers for user-defined child elements.

Performance Notes

  • Alignment classes are processed during widget build and when the class attribute changes
  • No runtime overhead once the layout is computed
  • Classes are parsed once and cached until the class attribute is modified
  • Bevy’s layout system handles the actual positioning efficiently

Migration from Manual Styling

If you were previously setting alignment properties manually:

#![allow(unused)]
fn main() {
// Old way
root_!(
    justify_content: JustifyContent::Center,
    align_items: AlignItems::Center;
    [ /* children */ ]
)

// New way with utility classes
root_!(
    class: "justify-center align-center";
    [ /* children */ ]
)
}

The utility class approach provides the same functionality with more concise syntax and better consistency across your application.

Routing

Routing

Routing in Makara is really easy. There are only 3 steps.

  1. Specifying route name on root widget.
  2. Register route.
  3. Navigating.

Specifying route

Just like I mentioned in previous chapter about hierarchy, we need to use root widget as a starting point for UI hierarchy.

If you want to have multiple pages or views, you need to create multiple starting points. In other words, each root defines one page or view.

For example, let’s say you want to have Home view and About Us view.

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, (setup_home_view, setup_about_us_view))
        .run();
}

fn setup_home_view(mut commands: Commands) {
    commands.spawn(
        root_!(route: "home"; [ text_!("This is home view") ])
    );
}

fn setup_about_us_view(mut commands: Commands) {
    commands.spawn(
        root_!(route: "about-us"; [ text_!("This is about us view") ])
    );
}

We can specify route name for each view by using route property.

Registering Routes

After we defined route on each view, we need to register those routes. We can do so by using Router resource.

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, (
            setup_home_view, 
            setup_about_us_view, 
            setup_routes // add here
        ))
        .run();
}

fn setup_routes(mut router: ResMut<Router>) {
    router.register_routes(["home", "about-us"]);
    
    // define which route you want to see when the application started
    router.default_route("home", ());
}

// setup view systems..

You may also want to set the default route. If you don’t, the first route in the list will be set to default automatically.

Route Navigation

Let’s update the above examples to demonstrate how to nagivate to specific route.

#![allow(unused)]
fn main() {
fn setup_home_view(mut commands: Commands) {
    commands.spawn(
        root_!(route: "home"; [
            text_!("This is home view"),
            button_!(
                "Go to about us view";
                on: |_clicked: On<Clicked>, mut router: ResMut<Router>| {
                    navigate_!(router, "about-us");
                }
            )
        ])
    );
}

fn setup_about_us_view(mut commands: Commands) {
    commands.spawn(
        root_!(route: "about-us"; [
            text_!("This is about us view"),
            button_!(
                "Go to home view";
                on: |_clicked: On<Clicked>, mut router: ResMut<Router>| {
                    navigate_!(router, "home");
                }
            )
        ])
    );
}
}

We can navigate to any route by calling navigate method.

Route Params

Of course, each route can have one or multiple params.

For example, let’s say we have a view called product-detail, but this view is dynamic based on product id or name.

#![allow(unused)]
fn main() {
fn setup_home_view(mut commands: Commands) {
    commands.spawn(
        root_!(route: "home"; [
            text_!("This is home view"),
            button_!(
                "See product";
                on: |_clicked: On<Clicked>, mut router: ResMut<Router>| { 
                    navigate_!(
                        router,
                        "product-detail",
                        Param::new()
                            .value("id", "1234") // just random product id
                            .value("name", "Iphone 17 256GB") // product name
                    )
                }
            )
        ])
    );
}

fn setup_product_detail_view(mut commands: Commands) { 
    commands.spawn(
        root_!(
            route: "product-detail";
            
            on: |page_loaded: On<PageLoaded>, mut text_q: TextQuery| {
                if let Some(text) = text_q.find_by_id("product-name") {
                    if let Some(name) = page_loaded.param.get("name") { 
                        text.text.value.0 = name.clone();
                    } 
                }
            };
            
            [ text_!("Product name: Unknown", id: "product-name") ]
        )
    );
}
}

See Router and Param for more info.

Organizing your project

Organizing your project

UI hierarchy can become verbosity and hard to keep track of. It’s always recommended to seperate your code into snippets.

For example,

use makara::prelude::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(MakaraPlugin::default())
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {    
    commands.spawn(
        root_!(
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center;
            
            [
                text_!("Register your info"),
                input_container(),
                button_container()
            ]
        )
    );
}

fn input_container() -> impl Bundle {
    column_!([
        text_input_!("Enter email"),
        text_input_!("Enter name"),
        text_!("Choose gender"),
        radio_group_!([ radio_!("Male"), radio_!("Female") ])
    ])
}

fn button_container() -> impl Bundle {
    row_!([
        button_!("Cancel"; on: |_clicked: On<Clicked>| {}),
        button_!("Submit"; on: |_clicked: On<Clicked>| {})
    ])
}