Start · 10 minutes
Quick start
From an empty project to a window that reacts to clicks and paints a real widget. All in one file and without the Selene compositor — any Wayland compositor will do.
What you need
| Component | Requirement | Why |
|---|---|---|
| System | Linux | Wayland, evdev, wl_shm. |
| Rust | 1.80+ (stable) | A rustup toolchain is enough. |
| Compositor | Wayland | Selene, GNOME, KDE, Sway — anything with xdg-shell. |
| Fonts | Adwaita Sans / DejaVu | The text engine looks for fonts installed on the system. |
You do not need GTK, Qt, GObject or pkg-config. libLuna links like any other Rust library.
Add the dependency
A typical application only needs the libluna facade — it re-exports everything else. Inside the monorepo, use a path dependency:
[dependencies]
libluna = { path = "../libluna/crates/libluna" }
# or from the repository:
# libluna = { git = "https://github.com/mijsys/libLuna.git" }
# eventually, from a registry:
# libluna = "0.0.4"
One use is enough:
use libluna::prelude::*;
Scaffolder
The fastest way to start is the bundled template:
./libluna/tools/libluna-new.sh ~/projects/my-app
cd ~/projects/my-app
cargo run
The first window
This program opens an 800×600 window, picks the locale from the environment, paints the background and draws text. That is the whole contract: the platform calls your closure on every frame, and you fill the scene.
use libluna::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
set_locale(Locale::from_env());
let config = AppConfig::new("My app")
.initial_size(800, 600)
.app_id("eu.luna.MyApp");
run(config, |frame: &mut Frame| {
let theme = frame.theme;
let (w, h) = (frame.size.width, frame.size.height);
frame.scene.fill_rect(
Rect::from_xywh(0.0, 0.0, w, h),
theme.colors.window_bg,
);
frame.scene.text(
TextRun::new(Point::new(24.0, 64.0), "Hello, libLuna", 24.0)
.with_weight(700)
.with_color(theme.colors.text),
);
})
}
frame.scene is cleared before every frame. Paint the whole frame from scratch — you do not need to call clear(), but you cannot rely on anything left from the previous frame.
Run and look
# your application
cargo run
# the example from the libLuna repository
cargo run --manifest-path libluna/Cargo.toml -p libluna --example minimal
# the widget gallery without opening a window (PNG)
cargo run --manifest-path apps/luna-demo/Cargo.toml --bin gallery
# a screenshot of any demo screen
cargo run --manifest-path apps/luna-demo/Cargo.toml --bin render -- docs/images/out.png
If no window appears, check WAYLAND_DISPLAY and make sure the session really runs on Wayland. libLuna has no X11 backend — XWayland only serves X11 applications inside Selene.
What the app receives: Frame
Frame is the whole context of one frame. The most important fields:
| Field | Type | Meaning |
|---|---|---|
scene | &mut Scene | Scene graph to fill; logical pixels. |
theme | Theme | Resolved theme: palette, spacing, radius, typography, metrics. |
text | &TextEngine | Text measurement and rasterisation (system or mono). |
size | Size | Current surface size in logical pixels. |
scale | f64 | Physical/logical scale (e.g. 2.0 on HiDPI). |
events | &[Event] | Events since the previous frame. |
dt | f64 | Time since the previous frame, in seconds. |
Common methods
| Method | Purpose |
|---|---|
request_frame() | Ask for another frame while an animation is running. |
request_frame_after(delay) | Schedule a frame for a clock or timeout. |
measure_text(text, size, weight) | Measure text without drawing it. |
set_title(...), close() | Window title and closing an xdg-toplevel. |
begin_move(), begin_resize(edges) | Dragging and resizing from your own CSD. |
copy_to_clipboard(...), paste_from_clipboard() | System clipboard; a paste arrives as Event::Paste. |
set_preference(...), set_accent(...) | Change theme and accent; persisted in the config. |
waker() | Wake the event loop safely from another thread. |
The first event
Events live in frame.events. This example toggles a colour on a left click:
use libluna::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::new("Click").initial_size(640, 400);
let mut active = false;
run(config, move |frame: &mut Frame| {
for event in frame.events {
if let Event::PointerButton(pe) = event {
if pe.button == PointerButton::Left
&& pe.state == ButtonState::Pressed
{
active = !active;
}
}
}
let theme = frame.theme;
let (w, h) = (frame.size.width, frame.size.height);
let color = if active { theme.colors.accent } else { theme.colors.card_bg };
frame.scene.fill_rect(Rect::from_xywh(0.0, 0.0, w, h), theme.colors.window_bg);
frame.scene.fill_rounded_rect(
Rect::from_xywh(24.0, 24.0, w - 48.0, h - 48.0),
theme.radius.large,
color,
);
})
}
Event kinds
| Variant | Data |
|---|---|
PointerButton | Position, button, state, modifiers. |
PointerMotion | Position and modifiers (for hover). |
PointerLeave | The pointer left the surface. |
Scroll | dx/dy in steps. |
Key | Keysym, keycode, text, state, modifiers, repeat. |
Text, Paste | Composed text and clipboard paste. |
Window | Resize, ScaleChanged, Focused, CloseRequested, Redraw. |
Drawing the scene
Scene is a list of commands, not retained state. Every method returns &mut Self, so calls can be chained.
| Method | Effect |
|---|---|
fill_rect(rect, color) | Filled rectangle. |
fill_rounded_rect(rect, radius, color) | Rounded rectangle. |
stroke_rounded_rect(...) | Rounded rectangle outline. |
stroke_line(from, to, width, color) | Line segment. |
fill_circle(center, radius, color) | Filled circle. |
stroke_circle(...), stroke_polyline(...), fill_polygon(...) | Outlines and polygons. |
text(TextRun) | Text with origin.y as the baseline. |
image(rect, data) | RGBA image inside a rectangle. |
push_clip(rect) / pop_clip() | Clip drawing (rounded variants too). |
frame.scene.text(
TextRun::new(Point::new(24.0, 48.0), "Title", theme.typography.title.size)
.with_weight(theme.typography.title.weight)
.with_color(theme.colors.text),
);
Remember: origin.y is the baseline of the first line, not the top edge of the text.
The first widget
Widgets are immediate: they keep state in an ordinary struct, and you call paint and click_at. There is no object tree and no manual refcounting.
use libluna::prelude::*;
use libluna::widgets::{Button, ButtonVariant, PaintCtx};
fn draw_ui(scene: &mut Scene, theme: &Theme, text: &TextEngine) {
let rect = Rect::from_xywh(24.0, 24.0, 120.0, 36.0);
let mut button = Button::new("Save")
.variant(ButtonVariant::Suggested);
let mut ctx = PaintCtx::new(scene, theme, text);
button.paint(&mut ctx, rect);
}
fn handle(event: &Event) -> bool {
let rect = Rect::from_xywh(24.0, 24.0, 120.0, 36.0);
let button = Button::new("Save");
match event {
Event::PointerButton(pe) => button.click_at(rect, pe.position),
_ => false,
}
}
In practice you keep the widget in application state (so it remembers hover/pressed and its value) instead of recreating it on every click. Full examples: Widget library.
Layout without manual maths
Above the widgets there is a retained layout tree: you build Nodes and the engine measures them and assigns rectangles to leaves.
use libluna::widgets::layout::{Layout, LayoutCtx, Node};
let tree = Node::padding(
Insets::splat(16.0),
Node::vbox(vec![
Node::label("Settings", theme.typography.heading),
Node::spacer(),
Node::hbox(vec![
Node::fixed(120.0, 34.0),
Node::spacer(),
Node::fixed(80.0, 34.0),
])
.gap(8.0),
])
.gap(10.0),
);
let layout_ctx = LayoutCtx { theme: &theme, text: frame.text };
let rects = Layout::new(tree).fit(
Rect::from_xywh(0.0, 0.0, frame.size.width, frame.size.height),
&layout_ctx,
);
rects contains leaf rectangles in DFS order — map them to your widgets. Details: API → layout.
Translations from day one
The key is the English string. A missing translation falls back to English, and every language is a separate file.
use libluna::t;
use libluna::prelude::*;
set_locale(Locale::from_env());
let title = tr("Settings");
let hello = tr_args("Hello, {name}", &[("name", "Ania")]);
let shortcut = t!("Settings");
| File | Content |
|---|---|
i18n/pl.lang | __name__ = Polski and Settings = Ustawienia. |
~/.config/luna/i18n/*.lang | Extra languages loaded at startup; they override built-in ones. |
Built-in catalogues are pl, en and de. More: Concepts → i18n.
Next steps
Understand the model
Frame, scene, immediate widgets, layout and events — without guessing.
Concepts →Pick a widget
77 components with previews and code. See what you already have.
Library →Adjust the look
Tokens, accents and metrics — without rewriting components.
Themes →Check the API
A complete, searchable index of types, functions and methods.
API →