libLuna Dokumentacja
PL EN
GitHub

Model mentalny

Klatka, nie drzewo.

libLuna nie ukrywa przed Tobą rysowania. Platforma daje Ci klatkę, Ty zmieniasz stan i budujesz scenę. Widgety to zwykłe struktury, które potrafią się narysować i odpowiedzieć na trafienie. To cała filozofia.

Pętla jednej klatki

Nie ma „ukrytego” drzewa, które trzeba aktualizować. Stan aplikacji jest u Ciebie. Scena jest budowana od zera w każdej klatce. Jeśli nic się nie zmienia, platforma nie woła domknięcia — chyba że poprosisz o klatkę.

Frame — kontekst klatki

Każde wywołanie domknięcia dostaje &mut Frame. To jednocześnie kontekst rysowania i nasłuch zdarzeń.

rust · sygnatura
pub fn run<F>(config: AppConfig, render: F) -> Result<(), Box<dyn Error>>
where
    F: FnMut(&mut Frame<'_>) + 'static;

Domknięcie jest FnMut, więc może trzymać i zmieniać własny stan (np. let mut selected = 0; przed run). Nie ma globalnego magazynu ani ręcznego Rc<RefCell<…>>.

Potrzebujesz animacji? Wywołaj frame.request_frame() w klatce, w której animacja trwa. Użyj frame.dt jako kroku czasu.

Scena to lista poleceń

Scene nie przechowuje stanu widżetów. Przechowuje DrawCommand — wystarczająco dużo, by renderer narysował piksele, i wystarczająco mało, by dało się ją przetestować bez okna.

GrupaPolecenia
KształtyFillRect, FillRoundedRect, StrokeRoundedRect, StrokeLine, FillCircle, StrokeCircle, StrokePolyline, FillPolygon.
OgraniczeniaPushClip, PushClipRounded, PopClip.
TreśćText(TextRun), Image, Background.

Scena jest niezależna od backendu. Dziś rasteryzuje ją SoftwareRenderer (tiny-skia + fontdue). Backend GPU może powstać bez zmian w widgetach, bo kontrakt to nadal lista poleceń.

Widgety immediate

Widget nie jest węzłem w drzewie i nie ma rodzica. Jest strukturą ze stanem prezentacji, którą aplikacja trzyma tam, gdzie jej wygodnie.

rust · typowy kontrakt
impl Button {
    pub fn new(label: impl Into<String>) -> Self;
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect);
    pub fn hit(&self, rect: Rect, p: Point) -> bool;
    pub fn click_at(&self, rect: Rect, p: Point) -> bool;
}

paint nie zmienia stanu. click_at mówi tylko, czy kliknięcie trafiło; decyzję o zmianie stanu podejmuje aplikacja. Wyjątki (np. TabBar, Entry) przyjmują &mut self, bo muszą zapamiętać kursor, zaznaczenie albo aktywną zakładkę.

rust · stan po stronie aplikacji
let mut button = Button::new("Zapisz").variant(ButtonVariant::Suggested);

run(config, move |frame: &mut Frame| {
    let rect = Rect::from_xywh(24.0, 24.0, 120.0, 36.0);
    for event in frame.events {
        if let Event::PointerButton(pe) = event {
            if pe.button == PointerButton::Left
                && pe.state == ButtonState::Pressed
                && button.click_at(rect, pe.position)
            {
                save();
            }
        }
    }

    let theme = frame.theme;
    let mut ctx = PaintCtx::new(frame.scene, &theme, frame.text);
    button.paint(&mut ctx, rect);
});

Immediate nie znaczy „bezstanowe”. Hover, pressed i fokus nadal trzeba ustawić na widżecie (set_hovered, set_pressed) albo trzymać we własnym stanie. libLuna nie robi tego automatycznie za całe drzewo.

Layout: measure → allocate

Ręczne liczenie prostokątów skaluje się źle. Dlatego nad widgetami jest retencyjne drzewo Node z dwoma przebiegami:

  1. measure — ile miejsca chce poddrzewo, biorąc pod uwagę Constraints (min/max).
  2. allocate — gdzie trafiają liście. Spacer i Expanded pochłaniają nadmiar wzdłuż osi stosu.
rust · drzewo układu
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, &layout_ctx);

rects to prostokąty liści w kolejności DFS. Aplikacja mapuje je na konkretne widgety. To celowo prosty kontrakt: layout nie wie, co rysuje liść, a widget nie wie, gdzie jest w drzewie.

Zdarzenia i trafianie

Platforma daje jedną listę Event na klatkę. Nie ma automatycznej propagacji po drzewie — to Ty decydujesz, które widgety dostaną zdarzenie i w jakiej kolejności.

EtapCo robisz
HoverNa PointerMotion sprawdzasz hit(rect, pos) i ustawiasz podświetlenie.
KlikNa PointerButton { Pressed } wołasz click_at / *_at.
KlawiaturaNa Event::Key sprawdzasz keysym, text, modyfikatory i fokus.
FokusTrzymasz identyfikator aktywnego widgetu. libLuna nie ma globalnego menedżera fokusu.
ZamknięcieEvent::Window(WindowEvent::CloseRequested) → np. frame.close().

To jest świadomy kompromis M0–M2: mniej magii, więcej kontroli. Gdy layout i drzewo staną się retencyjne, część routingu przejmie biblioteka, ale kontrakt paint/hit zostanie.

Motyw jest danymi

Theme to komplet tokenów: paleta, odstępy, promienie, typografia i metryki kontrolek. Widgety czytają z niego geometrię, więc motyw zmienia nie tylko kolory, ale i rozmiary.

PoleZawartość
colorswindow_bg, view_bg, card_bg, text, accent, border, stany itd.
spacingxs=3, sm=6, md=12, lg=18, xl=24.
radiussmall=6, medium=9, large=12, pill.
typographytitle, heading, body, caption.
metricsGeometria per widget: button.height, switch.width, headerbar.height…
appearanceLight lub Dark.

Style mogą też przyjść z plików *.theme. Szczegóły i przykłady: Motywy i tokeny.

i18n bez gettext

Klucz to angielski napis, a tłumaczenie to zwykły plik klucz = wartość. Brak wpisu w języku użytkownika spada do angielskiego.

lang · i18n/pl.lang
__name__ = Polski
Appearance = Wygląd
Settings = Ustawienia
Hello, {name} = Witaj, {name}
rust
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");

Wbudowane katalogi: pl, en, de. Dodatkowe pliki ładowane z ~/.config/luna/i18n, ~/.local/share/luna/i18n i /usr/share/luna/i18n nadpisują wbudowane.

Co jest świadomie proste

Brak automatycznego fokusu

Aplikacja decyduje, który widget ma fokus i jak go przenosić. To zostawia miejsce na własną politykę.

Brak drzewa dostępności

AT-SPI jest zaplanowane później. Dziś nie ma mostka do czytników ekranu.

Tekst bez złożonego shaping'u

fontdue obsługuje podstawowe pismo. Docelowo cosmic-text, bez zmiany kontraktu measure/draw.

Render CPU

Bufor trafia przez wl_shm. GPU/dmabuf jest w planach, ale nie zmieni API sceny.