Getting Started
Prerequisites
- Rust 1.85+ (edition 2024) — install via rustup
- Platform dependencies:
macOS: Xcode command line tools
xcode-select --install
Linux (Ubuntu/Debian):
sudo apt-get install -y \
libxkbcommon-dev libwayland-dev libxcb1-dev \
libvulkan-dev libfontconfig1-dev
Windows: Visual Studio Build Tools with C++ workload
Create a new project
cargo new my_app
cd my_app
Add Kael to Cargo.toml:
[dependencies]
kael = "0.1"
Your first window
Replace src/main.rs with:
use kael::*;
use kael::prelude::*;
struct Counter {
count: i32,
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let entity = cx.entity();
div()
.size_full()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap_4()
.bg(rgb(0x1E1E1E))
.text_color(rgb(0xFFFFFF))
.child(
div().text_3xl().child(format!("Count: {}", self.count))
)
.child(
div()
.flex()
.gap_2()
.child(
button("decrement")
.label("-1")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.count -= 1;
cx.notify();
});
}
})
)
.child(
button("increment")
.label("+1")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.count += 1;
cx.notify();
});
}
})
)
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(400.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Counter { count: 0 }),
).unwrap();
cx.activate(true);
});
}
Run it:
cargo run
What just happened
Application::new().run()— boots the platform event loopcx.open_window()— creates a native window with GPU renderingcx.new(|_| Counter { count: 0 })— creates a reactiveEntity<Counter>impl Render for Counter— defines what the entity draws each framecx.notify()— tells the framework to re-render after state changes
Key patterns
Builder pattern for UI
Every element uses method chaining. No JSX, no templates — just Rust:
#![allow(unused)]
fn main() {
div()
.flex()
.flex_col()
.gap_4()
.p_4()
.bg(rgb(0x1E1E1E))
.text_color(rgb(0xFFFFFF))
.child("Hello")
}
Reactive state
State lives in your struct. Mutate it, call cx.notify(), and the framework re-renders:
#![allow(unused)]
fn main() {
entity.update(cx, |this, cx| {
this.count += 1;
cx.notify();
});
}
Custom rendering
Every widget accepts .render_with() for full visual control:
#![allow(unused)]
fn main() {
button("save")
.label("Save")
.render_with(|state, _window, _cx| {
div()
.px_4().py_2()
.rounded(px(8.0))
.bg(if state.focused { rgb(0x2563eb) } else { rgb(0x3b82f6) })
.text_color(rgb(0xffffff))
.child(state.label.unwrap_or_default())
.into_any_element()
})
}
Next steps
- Core Concepts — understand Entity, Context, and the render cycle
- Form Controls — buttons, inputs, checkboxes, sliders, and more
- Platform APIs — file dialogs, system tray, notifications