🔒 Repository is read-only – file editing is disabled.
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(())
}