🔒 Repository is read-only – file editing is disabled.

luna/apps/luna-demo/src/bin/layout.rs main

228 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
//! Przykład: okno (karta ustawień) zbudowane **wyłącznie** z drzewa układu
//! (`measure`/`allocate`) — bez ręcznego liczenia prostokątów.
//!
//! Uruchom: `cargo run -p luna-demo --bin layout [--theme PATH|NAME] [--out ŚCIEŻKA]`
//! Efekt:   `docs/images/layout.png` (albo wskazane `--out`).

use std::error::Error;

use libluna_core::style::{weight, FontStyle};
use libluna_core::{Insets, Point, Rect, Size, Theme};
use libluna_render::{Scene, SoftwareRenderer, TextEngine};
use libluna_widgets::icon;
use libluna_widgets::layout::{Layout, LayoutCtx, Node};
use libluna_widgets::PaintCtx;

const W: u32 = 560;
const H: u32 = 420;
const PAD: f64 = 16.0;

/// Wiersz ustawień: etykiety po lewej, kontrolka (wyśrodkowana) po prawej.
fn row(label: &str, style: FontStyle, control: Node) -> Node {
    Node::padding(
        Insets::symmetric(12.0, PAD),
        Node::hbox(vec![
            Node::label(label, style),
            Node::spacer(),
            Node::center(control),
        ])
        .gap(12.0),
    )
}

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let mut theme_spec: Option<String> = None;
    let mut out = "docs/images/layout.png".to_owned();
    let mut i = 0;
    while i < args.len() {
        match args[i].as_str() {
            "--theme" => {
                if let Some(v) = args.get(i + 1) {
                    theme_spec = Some(v.clone());
                }
                i += 2;
            }
            "--out" => {
                if let Some(v) = args.get(i + 1) {
                    out = v.clone();
                }
                i += 2;
            }
            _ => i += 1,
        }
    }

    let mut theme = Theme::dark();
    if let Some(spec) = &theme_spec {
        match libluna_core::theme_file::load_spec(spec) {
            Some(patch) => theme = patch.build(theme),
            None => eprintln!("theme not found: {spec}"),
        }
    }
    let text = TextEngine::system();
    let size = Size::new(W as f64, H as f64);
    let ctx = LayoutCtx {
        theme: &theme,
        text: &text,
    };

    // --- Drzewo układu (retencyjne, dwa przebiegi: measure → allocate) ---
    let tree = Node::padding(
        Insets::splat(PAD),
        Node::vbox(vec![
            Node::padding(
                Insets::symmetric(14.0, PAD),
                Node::label("Appearance", theme.typography.heading),
            ),
            Node::fixed(0.0, 1.0), // separator
            row("Theme", theme.typography.body, Node::fixed(160.0, 32.0)),
            Node::fixed(0.0, 1.0),
            row("Volume", theme.typography.body, Node::fixed(160.0, 32.0)),
            Node::fixed(0.0, 1.0),
            row("Language", theme.typography.body, Node::fixed(160.0, 32.0)),
            Node::spacer(),
            Node::padding(
                Insets::symmetric(12.0, PAD),
                Node::hbox(vec![Node::spacer(), Node::center(Node::fixed(120.0, 34.0))]).gap(12.0),
            ),
        ]),
    );

    let rects = Layout::new(tree).fit(Rect::from_size(size), &ctx);

    let mut scene = Scene::new();
    let mut paint = PaintCtx {
        scene: &mut scene,
        theme: &theme,
        text: &text,
    };
    let colors = theme.colors;

    // Tło okna i karty.
    paint
        .scene
        .fill_rect(Rect::from_size(size), colors.window_bg);
    let card = Rect::from_size(size).deflate(Insets::splat(PAD));
    paint
        .scene
        .fill_rounded_rect(card, theme.radius.large, colors.card_bg);
    paint
        .scene
        .stroke_rounded_rect(card, theme.radius.large, 1.0, colors.border);

    // Liście w kolejności DFS (zgodnie ze strukturą drzewa powyżej):
    // 0 nagłówek · 1/5/9 separatory · 2/6/10 etykiety · 4/8/12 kontrolki · 15 przycisk.
    let heading = theme.typography.heading.size;
    let body = theme.typography.body.size;
    let caption = theme.typography.caption.size;

    paint.label_left(
        rects[0].x(),
        rects[0].center().y,
        "Appearance",
        heading,
        weight::BOLD,
        colors.text,
    );

    for sep in [1, 5, 9] {
        let r = rects[sep];
        paint.scene.fill_rect(
            Rect::from_xywh(r.x(), r.center().y, r.width(), 1.0),
            colors.border,
        );
    }

    // Etykiety wierszy.
    for (i, name) in [(2, "Theme"), (6, "Volume"), (10, "Language")] {
        let r = rects[i];
        paint.label_left(r.x(), r.center().y, name, body, weight::NORMAL, colors.text);
    }

    // Kontrolka 4 — pole z chevronem (jak segmentowy przełącznik).
    let seg = rects[4];
    paint
        .scene
        .fill_rounded_rect(seg, theme.radius.small, colors.shade);
    paint.label_center(
        seg.x() + seg.width() * 0.4,
        seg.center().y,
        "Dark",
        body,
        weight::MEDIUM,
        colors.text,
    );
    icon::chevron_down(
        &mut paint,
        colors.dim_text,
        Point::new(seg.right() - 14.0, seg.center().y),
        4.0,
        2.0,
    );

    // Kontrolka 8 — suwak głośności.
    let vol = rects[8];
    let track = Rect::from_xywh(vol.x(), vol.center().y - 3.0, vol.width(), 6.0);
    paint
        .scene
        .fill_rounded_rect(track, 3.0, colors.shade.scale_alpha(1.8));
    let filled = Rect::from_xywh(track.x(), track.y(), track.width() * 0.65, track.height());
    paint.scene.fill_rounded_rect(filled, 3.0, colors.accent);
    paint.scene.fill_rounded_rect(
        Rect::from_xywh(filled.right() - 9.0, vol.center().y - 9.0, 18.0, 18.0),
        9.0,
        colors.accent_fg,
    );
    paint.label_center(
        vol.x() - 28.0,
        vol.center().y,
        "65%",
        caption,
        weight::NORMAL,
        colors.text,
    );

    // Kontrolka 12 — lista rozwijana (pole + chevron).
    let dd = rects[12];
    paint
        .scene
        .fill_rounded_rect(dd, theme.radius.small, colors.shade);
    paint.label_left(
        dd.x() + 12.0,
        dd.center().y,
        "Polski",
        body,
        weight::NORMAL,
        colors.text,
    );
    icon::chevron_down(
        &mut paint,
        colors.dim_text,
        Point::new(dd.right() - 16.0, dd.center().y),
        4.0,
        2.0,
    );

    // Przycisk 15 — akcja sugerowana.
    let btn = rects[15];
    paint
        .scene
        .fill_rounded_rect(btn, theme.radius.small, colors.accent);
    paint.label_center(
        btn.center().x,
        btn.center().y,
        "Save changes",
        body,
        weight::MEDIUM,
        colors.accent_fg,
    );

    // Render do PNG.
    let mut renderer = SoftwareRenderer::new(W, H);
    renderer.draw(&scene, 1.0, colors.window_bg);
    let png = renderer.encode_png().ok_or("failed to encode PNG")?;
    std::fs::write(&out, png)?;
    println!("saved {out} ({W}x{H}, {} leaves)", rects.len());
    Ok(())
}