libLuna Dokumentacja
PL EN
GitHub

Start · 10 minut

Szybki start

Od pustego projektu do okna, które reaguje na kliknięcie i rysuje prawdziwy widget. Wszystko w jednym pliku i bez kompozytora Selene — wystarczy dowolny Wayland.

Co będzie potrzebne

SkładnikWymaganiePo co
SystemLinuxWayland, evdev, wl_shm.
Rust1.80+ (stable)Toolchain z rustup wystarczy.
KompozytorWaylandSelene, GNOME, KDE, Sway — każdy z xdg-shell.
CzcionkiAdwaita Sans / DejaVuSilnik tekstu szuka fontu w systemie.

Nie potrzebujesz GTK, Qt, GObject ani pkg-config. libLuna linkuje się jak zwykła biblioteka Rusta.

Dodaj zależność

W typowej aplikacji wystarczy fasada libluna — ona re-eksportuje całą resztę. W repozytorium monorepo użyj ścieżki:

toml · Cargo.toml
[dependencies]
libluna = { path = "../libluna/crates/libluna" }
# albo z repozytorium:
# libluna = { git = "https://github.com/mijsys/libLuna.git" }
# docelowo, z rejestru:
# libluna = "0.0.4"

W kodzie wystarczy jedno use:

rust
use libluna::prelude::*;

Scaffolder

Najszybciej utworzysz projekt gotowym szablonem:

bash
./libluna/tools/libluna-new.sh ~/projekty/moja-apka
cd ~/projekty/moja-apka
cargo run

Pierwsze okno

Poniższy program otwiera okno 800×600, ustawia język z systemu, rysuje tło i napis. To cały kontrakt: platforma woła domknięcie przy każdej klatce, a Ty wypełniasz scenę.

rust · src/main.rs
use libluna::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    set_locale(Locale::from_env());

    let config = AppConfig::new("Moja apka")
        .initial_size(800, 600)
        .app_id("eu.luna.MojaApka");

    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 jest czyszczona przed każdą klatką. Aplikacja rysuje całą klatkę od zera — nie trzeba wołać clear(), ale nie można liczyć na ślady z poprzedniej klatki.

Uruchom i obejrzyj

bash
# Twoja aplikacja
cargo run

# Przykład z repozytorium libLuny
cargo run --manifest-path libluna/Cargo.toml -p libluna --example minimal

# Galeria widgetów bez otwierania okna (PNG)
cargo run --manifest-path apps/luna-demo/Cargo.toml --bin gallery

# Zrzut dowolnego ekranu demo
cargo run --manifest-path apps/luna-demo/Cargo.toml --bin render -- docs/images/out.png

Jeśli okno się nie pojawia, sprawdź WAYLAND_DISPLAY i to, czy sesja naprawdę działa pod Waylandem. libLuna nie ma backendu X11 — XWayland obsługuje tylko okna innych aplikacji w Selene.

Co dostaje aplikacja: Frame

Frame to cały kontekst jednej klatki. Najważniejsze pola:

PoleTypZnaczenie
scene&mut SceneGraf sceny do wypełnienia; piksele logiczne.
themeThemeRozwiązany motyw: paleta, spacing, radius, typografia, metryki.
text&TextEnginePomiar i rasteryzacja tekstu (systemowy lub mono).
sizeSizeBieżący rozmiar powierzchni w pikselach logicznych.
scalef64Skala fizyczna/logiczna (np. 2.0 na HiDPI).
events&[Event]Zdarzenia od poprzedniej klatki.
dtf64Czas od poprzedniej klatki w sekundach.

Najczęstsze metody

MetodaDo czego
request_frame()Poproś o kolejną klatkę, dopóki trwa animacja.
request_frame_after(delay)Zaplanuj klatkę np. dla zegara lub timeoutu.
measure_text(text, size, weight)Zmierz napis bez rysowania.
set_title(...), close()Tytuł i zamknięcie okna xdg-toplevel.
begin_move(), begin_resize(edges)Przeciąganie i zmiana rozmiaru z własnego CSD.
copy_to_clipboard(...), paste_from_clipboard()Schowek systemowy; wklejenie wraca jako Event::Paste.
set_preference(...), set_accent(...)Zmiana motywu i akcentu, zapisywana w konfiguracji.
waker()Bezpieczne wybudzenie pętli z innego wątku.

Pierwsze zdarzenie

Zdarzenia są w frame.events. Ten przykład przełącza kolor po kliknięciu lewym przyciskiem:

rust · obsługa kliknięcia
use libluna::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = AppConfig::new("Klik").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,
        );
    })
}

Rodzaje zdarzeń

WariantDane
PointerButtonPozycja, przycisk, stan, modyfikatory.
PointerMotionPozycja i modyfikatory (dla hoveru).
PointerLeaveKursor opuścił powierzchnię.
Scrolldx/dy w krokach.
KeyKeysym, keycode, tekst, stan, modyfikatory, repeat.
Text, PasteZłożony tekst oraz wklejenie ze schowka.
WindowResize, ScaleChanged, Focused, CloseRequested, Redraw.

Rysowanie sceny

Scene to lista poleceń, nie stos stanu. Każda metoda zwraca &mut Self, więc można je łączyć.

MetodaEfekt
fill_rect(rect, color)Wypełniony prostokąt.
fill_rounded_rect(rect, radius, color)Prostokąt z zaokrągleniami.
stroke_rounded_rect(...)Obramowanie zaokrąglonego prostokąta.
stroke_line(from, to, width, color)Odcinek.
fill_circle(center, radius, color)Wypełniony okrąg.
stroke_circle(...), stroke_polyline(...), fill_polygon(...)Obrysy i wielokąty.
text(TextRun)Tekst z linią bazową origin.y.
image(rect, data)Obraz RGBA w prostokącie.
push_clip(rect) / pop_clip()Ograniczenie rysowania (także zaokrąglone).
rust · tekst
frame.scene.text(
    TextRun::new(Point::new(24.0, 48.0), "Tytuł", theme.typography.title.size)
        .with_weight(theme.typography.title.weight)
        .with_color(theme.colors.text),
);

Pamiętaj: origin.y to linia bazowa pierwszego wiersza, nie górna krawędź tekstu.

Pierwszy widget

Widgety są immediate: trzymają stan w zwykłej strukturze, a Ty wołasz paint i click_at. Nie ma drzewa obiektów ani ręcznego refcountingu.

rust · Button
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("Zapisz")
        .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("Zapisz");

    match event {
        Event::PointerButton(pe) => button.click_at(rect, pe.position),
        _ => false,
    }
}

W praktyce trzymasz widget w stanie aplikacji (żeby pamiętał hover/pressed i wartość), a nie tworzysz go od nowa przy każdym kliknięciu. Pełne przykłady: Biblioteka widgetów.

Układ bez ręcznego liczenia

Nad widgetami jest retencyjny layout: budujesz drzewo Node, a silnik mierzy je i przydziela prostokąty liściom.

rust · Layout
use libluna::widgets::layout::{Layout, LayoutCtx, Node};

let tree = Node::padding(
    Insets::splat(16.0),
    Node::vbox(vec![
        Node::label("Ustawienia", 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 zawiera prostokąty liści w kolejności DFS — mapujesz je na swoje widgety. Szczegóły: API → layout.

Tłumaczenia od pierwszego dnia

Kluczem jest angielski napis. Brak tłumaczenia degraduje się do angielskiego, a każdy język to osobny plik.

rust · i18n
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");
PlikZawartość
i18n/pl.lang__name__ = Polski oraz Settings = Ustawienia.
~/.config/luna/i18n/*.langDodatkowe języki ładowane przy starcie, nadpisują wbudowane.

Wbudowane są pl, en i de. Więcej: Koncepcje → i18n.

Co dalej