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
Event
run(config, |frame| …)
zmieniasz stan i wypełniasz frame.scene
Scene
lista poleceń: kształty, clip, tekst, obrazy
SoftwareRenderer → wl_shm
CPU rysuje RGBA8888, kompozytor pokazuje bufor
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ń.
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.
| Grupa | Polecenia |
|---|---|
| Kształty | FillRect, FillRoundedRect, StrokeRoundedRect, StrokeLine, FillCircle, StrokeCircle, StrokePolyline, FillPolygon. |
| Ograniczenia | PushClip, 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.
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ę.
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:
- measure — ile miejsca chce poddrzewo, biorąc pod uwagę
Constraints(min/max). - allocate — gdzie trafiają liście.
SpaceriExpandedpochłaniają nadmiar wzdłuż osi stosu.
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.
| Etap | Co robisz |
|---|---|
| Hover | Na PointerMotion sprawdzasz hit(rect, pos) i ustawiasz podświetlenie. |
| Klik | Na PointerButton { Pressed } wołasz click_at / *_at. |
| Klawiatura | Na Event::Key sprawdzasz keysym, text, modyfikatory i fokus. |
| Fokus | Trzymasz identyfikator aktywnego widgetu. libLuna nie ma globalnego menedżera fokusu. |
| Zamknięcie | Event::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.
| Pole | Zawartość |
|---|---|
colors | window_bg, view_bg, card_bg, text, accent, border, stany itd. |
spacing | xs=3, sm=6, md=12, lg=18, xl=24. |
radius | small=6, medium=9, large=12, pill. |
typography | title, heading, body, caption. |
metrics | Geometria per widget: button.height, switch.width, headerbar.height… |
appearance | Light 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.
__name__ = Polski
Appearance = Wygląd
Settings = Ustawienia
Hello, {name} = Witaj, {name}
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.