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

luna/apps/luna-demo/src/gallery_page.rs main

352 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
//! Strona „Widgety” w demo — pełna galeria komponentów libLuny.
//!
//! To ta sama zawartość, którą pokazują `docs/images/gallery.png` i
//! `docs/images/new-widgets.png`: `NavigationRail`, `ColumnView`, `Calendar`,
//! przyciski/pola/wybory, `InfoBar`, `MediaControls`, `Picture`, `Expander`,
//! `Paned`, `Assistant`, `Scrollbar` i `Popover`.

use std::sync::{Arc, OnceLock};

use libluna_core::style::weight;
use libluna_core::{Color, Rect};
use libluna_render::ImageData;
use libluna_widgets::{
    Adjustment, Assistant, Avatar, Banner, BannerKind, Calendar, ColorButton, ColorChooser, Column,
    ColumnView, ContentFit, EmojiChooser, Expander, FlowBox, Image, ImageIcon, InfoBar,
    InfoBarKind, LinkButton, MediaControls, MenuButton, NavigationRail, Orientation, PaintCtx,
    Paned, PasswordEntry, Picture, Popover, PopoverPlacement, ProgressBar, Scrollbar, SpinButton,
    Spinner, ToggleButton,
};

/// Symbole rysowane fontem interfejsu (emoji kolorowe wymagają osobnego fontu).
const SYMBOLS: [&str; 12] = ["★", "♥", "♪", "☀", "✓", "✗", "✚", "●", "◆", "■", "▲", "☰"];

/// Rysuje galerię widgetów w podanym prostokącie (współrzędne wewnątrz `area`).
pub fn paint(paint: &mut PaintCtx, area: Rect) {
    let theme = *paint.theme;
    paint.scene.fill_rect(area, theme.colors.window_bg);

    let r = |x: f64, y: f64, w: f64, h: f64| Rect::from_xywh(area.x() + x, area.y() + y, w, h);
    let caption = |paint: &mut PaintCtx, x: f64, y: f64, text: &str| {
        paint.label_left(
            area.x() + x,
            area.y() + y + 8.0,
            text,
            paint.theme.typography.caption.size,
            weight::BOLD,
            paint.theme.colors.dim_text,
        );
    };

    // ── Wiersz 1: nawigacja, tabela, kalendarz ──
    caption(paint, 16.0, 4.0, "NavigationRail / ColumnView / Calendar");

    let rail = NavigationRail::new(["Overview", "Network", "System", "About"]);
    rail.paint(paint, r(16.0, 26.0, 180.0, 180.0));

    let mut table = ColumnView::new(vec![
        Column::new("Name").weight(2.0),
        Column::new("Size"),
        Column::new("Date"),
    ]);
    table.set_rows(5);
    table.set_selected(Some(1));
    let table_rect = r(216.0, 26.0, 420.0, 180.0);
    table.paint(paint, table_rect);
    for (i, name) in [
        "Projects",
        "Report.pdf",
        "Notes.txt",
        "Budget.xlsx",
        "Backup",
    ]
    .iter()
    .enumerate()
    {
        let cell = table.cell_rect(table_rect, &theme, i, 0);
        paint.label_left(
            cell.x() + theme.spacing.md,
            cell.center().y,
            name,
            theme.typography.body.size,
            weight::NORMAL,
            theme.colors.text,
        );
    }

    let calendar = Calendar::new(2024, 1);
    let weekdays: Vec<String> = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
        .iter()
        .map(|s| s.to_string())
        .collect();
    calendar.paint(
        paint,
        r(656.0, 26.0, 288.0, 180.0),
        "January 2024",
        &weekdays,
    );

    // ── Wiersz 2: przyciski, pola, choosery ──
    caption(paint, 16.0, 212.0, "Buttons / Entries / Choosers");

    let toggle = ToggleButton::new("Bold").active(true);
    toggle.paint(paint, r(16.0, 234.0, 90.0, toggle.height(&theme)));
    LinkButton::new("Documentation").paint(paint, r(116.0, 234.0, 130.0, 34.0));
    ColorButton::new(Color::from_rgba8(0x91, 0x41, 0xac, 0xff))
        .paint(paint, r(256.0, 238.0, 28.0, 26.0));
    MenuButton::burger().paint(paint, r(296.0, 234.0, 40.0, 34.0));
    MenuButton::new("Options").paint(paint, r(344.0, 234.0, 110.0, 34.0));
    SpinButton::new(65.0, 0.0, 100.0).paint(paint, r(464.0, 234.0, 110.0, 34.0));

    let mut password = PasswordEntry::new("Password");
    password.insert_str("hunter2");
    password.paint(paint, r(588.0, 234.0, 160.0, 34.0));

    for (i, icon) in [
        ImageIcon::Folder,
        ImageIcon::File,
        ImageIcon::Search,
        ImageIcon::Burger,
    ]
    .into_iter()
    .enumerate()
    {
        Image::new(icon)
            .size(28.0)
            .paint(paint, r(760.0 + i as f64 * 36.0, 234.0, 28.0, 34.0));
    }

    let mut chooser = ColorChooser::new(ColorChooser::default_palette());
    chooser.set_columns(4);
    let ch = chooser.height(220.0, &theme).min(96.0);
    chooser.paint(paint, r(16.0, 276.0, 220.0, ch));

    let symbols: Vec<String> = SYMBOLS.iter().map(|s| s.to_string()).collect();
    let mut emoji = EmojiChooser::new(symbols);
    emoji.set_columns(6);
    emoji.set_selected(1);
    let eh = emoji.height(400.0, &theme).min(96.0);
    emoji.paint(paint, r(256.0, 276.0, 400.0, eh));

    let mut flow = FlowBox::new(56.0);
    flow.set_count(8);
    flow.set_selected(Some(2));
    flow.paint(paint, r(676.0, 276.0, 252.0, 96.0));

    // ── Wiersz 3: InfoBar (4 rodzaje) ──
    caption(
        paint,
        16.0,
        396.0,
        "InfoBar — Info / Question / Warning / Error",
    );

    let bars = [
        (InfoBarKind::Info, "Nowa wersja jest dostępna", Some("OK")),
        (
            InfoBarKind::Question,
            "Zsynchronizować ustawienia?",
            Some("OK"),
        ),
        (InfoBarKind::Warning, "Mało miejsca na dysku", Some("OK")),
        (
            InfoBarKind::Error,
            "Nie udało się zapisać pliku",
            Some("Ponów"),
        ),
    ];
    for (i, (kind, msg, action)) in bars.iter().enumerate() {
        let mut bar = InfoBar::new(*msg).kind(*kind);
        if let Some(action) = action {
            bar = bar.action(*action);
        }
        let (col, row) = (i % 2, i / 2);
        bar.paint(
            paint,
            r(
                16.0 + col as f64 * 462.0,
                418.0 + row as f64 * 56.0,
                450.0,
                52.0,
            ),
        );
    }

    // ── Wiersz 4: MediaControls + Picture ──
    caption(paint, 16.0, 538.0, "MediaControls");
    caption(paint, 640.0, 538.0, "Picture — Contain / Cover / Fill");

    let mut media = MediaControls::new("Aurora", "Luna Ensemble");
    media.set_times("1:42", "4:07");
    media.set_position(0.41);
    media.paint(paint, r(16.0, 560.0, 600.0, 58.0));

    let mut paused = MediaControls::new("Nokturn", "Selene");
    paused.set_playing(false);
    paused.set_times("0:15", "3:20");
    paused.set_position(0.08);
    paused.paint(paint, r(16.0, 624.0, 600.0, 58.0));

    let image = gradient();
    for (i, fit) in [ContentFit::Contain, ContentFit::Cover, ContentFit::Fill]
        .into_iter()
        .enumerate()
    {
        let mut pic = Picture::with_image(image.clone());
        pic.set_fit(fit);
        let rect = r(640.0, 560.0 + i as f64 * 58.0, 288.0, 52.0);
        paint.scene.fill_rect(rect, theme.colors.view_bg);
        pic.paint(paint, rect);
        paint
            .scene
            .stroke_rounded_rect(rect, 0.0, 1.0, theme.colors.border);
    }

    // ── Pasek statusu: Banner + Spinner + ProgressBar + Avatar ──
    Banner::new("Nowa wersja libLuny")
        .kind(BannerKind::Info)
        .description("Wszystkie widgety z galerii są już dostępne.")
        .action("OK")
        .paint(paint, r(16.0, 688.0, 420.0, 52.0));

    let mut spinner = Spinner::new();
    spinner.set_progress(0.35);
    spinner.paint(paint, r(456.0, 702.0, 24.0, 24.0));
    ProgressBar::new()
        .fraction(0.65)
        .text("65%")
        .paint(paint, r(490.0, 710.0, 100.0, 8.0));
    Avatar::new("PL").paint(paint, r(606.0, 696.0, 32.0, 32.0));

    // ── Wiersz 5: Expander / Paned / Assistant + Scrollbar / Popover ──
    caption(
        paint,
        16.0,
        750.0,
        "Expander / Paned / Assistant / Scrollbar / Popover",
    );

    let expander = Expander::new("Advanced options").expanded(true);
    let exp_rect = r(16.0, 772.0, 220.0, 80.0);
    expander.paint(paint, exp_rect);
    let content = expander.content_rect(exp_rect, &theme);
    paint.label_left(
        content.x() + theme.spacing.md,
        content.y() + 16.0,
        "Extra settings…",
        theme.typography.caption.size,
        weight::NORMAL,
        theme.colors.dim_text,
    );

    let mut paned = Paned::new(Orientation::Horizontal);
    paned.set_position(0.4);
    let paned_rect = r(256.0, 772.0, 300.0, 80.0);
    let (a, b) = paned.panes(paned_rect, &theme);
    paint.scene.fill_rect(a, theme.colors.shade);
    paint.scene.fill_rect(b, theme.colors.card_bg);
    paned.paint(paint, paned_rect);

    Assistant::new(["Welcome", "Details", "Done"]).paint(
        paint,
        r(576.0, 772.0, 352.0, 80.0),
        "Cancel",
        "Next",
    );

    let mut bar = Scrollbar::new(Orientation::Horizontal);
    let mut adj = Adjustment::new(0.0, 200.0, 60.0);
    adj.set_fraction(0.35);
    bar.set_adjustment(adj);
    bar.paint(paint, r(16.0, 862.0, 300.0, 24.0));

    let popover = Popover::new(PopoverPlacement::Above);
    let pop_rect = r(560.0, 856.0, 200.0, 56.0);
    popover.paint(paint, pop_rect);
    paint.label_left(
        pop_rect.x() + theme.spacing.md,
        pop_rect.center().y,
        "Popover content",
        theme.typography.body.size,
        weight::NORMAL,
        theme.colors.text,
    );
}

/// Obraz gradientowy do demonstracji `Picture` (tworzony raz, współdzielony).
fn gradient() -> Arc<ImageData> {
    static IMAGE: OnceLock<Arc<ImageData>> = OnceLock::new();
    IMAGE
        .get_or_init(|| {
            let (w, h) = (160u32, 100u32);
            let mut rgba = Vec::with_capacity((w * h * 4) as usize);
            for y in 0..h {
                for x in 0..w {
                    let r = (x * 255 / w.max(1)) as u8;
                    let g = (y * 255 / h.max(1)) as u8;
                    rgba.extend_from_slice(&[r, g, 200, 255]);
                }
            }
            Arc::new(ImageData::new(w, h, rgba))
        })
        .clone()
}

/// Maksymalna wysokość strony galerii (do ewentualnego przewijania w przyszłości).
#[allow(dead_code)]
pub fn content_height() -> f64 {
    860.0
}

#[cfg(test)]
mod tests {
    use super::*;
    use libluna_core::Size;
    use libluna_render::{Scene, SoftwareRenderer, TextEngine};

    /// Podgląd galerii do PNG — pozwala obejrzeć stronę bez kompozytora.
    #[test]
    #[ignore]
    fn render_gallery_preview() {
        let theme = libluna_core::Theme::dark();
        let text = TextEngine::system();
        let size = Size::new(960.0, 940.0);
        let mut scene = Scene::new();
        {
            let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
            paint(&mut ctx, Rect::from_size(size));
        }
        let mut renderer = SoftwareRenderer::new(960, 940);
        renderer.draw(&scene, 1.0, theme.colors.window_bg);
        let png = renderer.encode_png().expect("PNG");
        std::fs::write("/tmp/luna-gallery-page.png", png).expect("zapis PNG");
    }

    /// Podgląd całego okna (sidebar + header + galeria) do PNG.
    #[test]
    #[ignore]
    fn render_demo_widgets_window() {
        use crate::ui::{build_scene, Demo, WIDGETS_NAV};
        use libluna_core::{Accent, Theme, ThemePreference};

        let theme = Theme::dark();
        let text = TextEngine::system();
        let size = Size::new(1200.0, 1028.0);
        let mut demo = Demo::default();
        demo.nav_selected = Some(WIDGETS_NAV);
        let mut scene = Scene::new();
        build_scene(
            &mut scene,
            &theme,
            &text,
            size,
            &demo,
            ThemePreference::Dark,
            Accent::default(),
        );
        let mut renderer = SoftwareRenderer::new(1200, 1028);
        renderer.draw(&scene, 1.0, theme.colors.window_bg);
        let png = renderer.encode_png().expect("PNG");
        std::fs::write("/tmp/luna-demo-widgets.png", png).expect("zapis PNG");
    }
}