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

luna/crates/libluna-widgets/src/navigation.rs main

279 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
//! `NavigationView` / `NavigationSplitView` / `OverlaySplitView` / `Sidebar` — nawigacja.
//!
//! Pomocniki układu i „chrome” nawigacji (jak `AdwNavigationView` i widoki dzielone):
//! wyznaczają prostokąty paneli i rysują pasek z przyciskiem „wstecz”.

use libluna_core::{Insets, Point, Rect, Theme};

use crate::ctx::PaintCtx;
use crate::icon;

/// Pasek nawigacji: „wstecz” + obszar tytułu + treść strony.
#[derive(Debug, Clone, Default)]
pub struct NavigationView {
    depth: usize,
}

impl NavigationView {
    pub fn new() -> Self {
        Self::default()
    }

    /// Głębokość stosu (0 = strona główna).
    pub fn depth(&self) -> usize {
        self.depth
    }

    pub fn can_pop(&self) -> bool {
        self.depth > 0
    }

    pub fn push(&mut self) {
        self.depth += 1;
    }

    /// Zdejmuje stronę; zwraca `true`, jeśli coś zdjęto.
    pub fn pop(&mut self) -> bool {
        if self.depth > 0 {
            self.depth -= 1;
            true
        } else {
            false
        }
    }

    fn bar_height(theme: &Theme) -> f64 {
        theme.metrics.headerbar.height
    }

    /// Pasek nawigacji (nad treścią strony).
    pub fn bar_rect(&self, rect: Rect, theme: &Theme) -> Rect {
        Rect::from_xywh(rect.x(), rect.y(), rect.width(), Self::bar_height(theme))
    }

    /// Prostokąt treści strony (poniżej paska).
    pub fn content_rect(&self, rect: Rect, theme: &Theme) -> Rect {
        let h = Self::bar_height(theme);
        Rect::from_xywh(
            rect.x(),
            rect.y() + h,
            rect.width(),
            (rect.height() - h).max(0.0),
        )
    }

    /// Prostokąt przycisku „wstecz” (gdy jest co zdjąć).
    pub fn back_rect(&self, rect: Rect, theme: &Theme) -> Option<Rect> {
        if !self.can_pop() {
            return None;
        }
        let bar = self.bar_rect(rect, theme);
        let control = theme.metrics.headerbar.control;
        Some(Rect::from_xywh(
            rect.x() + theme.spacing.md,
            bar.center().y - control / 2.0,
            control,
            control,
        ))
    }

    pub fn back_at(&self, rect: Rect, theme: &Theme, p: Point) -> bool {
        self.back_rect(rect, theme).is_some_and(|r| r.contains(p))
    }

    /// Rysuje pasek nawigacji (tło + „wstecz”). Tytuł rysuje aplikacja (np. `WindowTitle`).
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
        let theme = *ctx.theme;
        let bar = self.bar_rect(rect, &theme);
        ctx.scene.fill_rect(bar, theme.colors.headerbar_bg);
        ctx.scene.fill_rect(
            Rect::from_xywh(bar.x(), bar.bottom() - 1.0, bar.width(), 1.0),
            theme.colors.border,
        );
        if let Some(b) = self.back_rect(rect, &theme) {
            icon::chevron_left(ctx, theme.colors.text, b.center(), 4.0, 2.0);
        }
    }
}

/// Widok dzielony: panel boczny + treść (panel można schować).
#[derive(Debug, Clone)]
pub struct NavigationSplitView {
    show_sidebar: bool,
    sidebar_width: f64,
}

impl Default for NavigationSplitView {
    fn default() -> Self {
        Self {
            show_sidebar: true,
            sidebar_width: 220.0,
        }
    }
}

impl NavigationSplitView {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn sidebar_width(mut self, width: f64) -> Self {
        self.sidebar_width = width;
        self
    }

    pub fn show_sidebar(&self) -> bool {
        self.show_sidebar
    }

    pub fn set_show_sidebar(&mut self, show: bool) {
        self.show_sidebar = show;
    }

    pub fn toggle(&mut self) {
        self.show_sidebar = !self.show_sidebar;
    }

    /// Prostokąt panelu bocznego (gdy widoczny).
    pub fn sidebar_rect(&self, rect: Rect) -> Option<Rect> {
        self.show_sidebar
            .then(|| Rect::from_xywh(rect.x(), rect.y(), self.sidebar_width, rect.height()))
    }

    /// Prostokąt treści (obok panelu, albo cały gdy schowany).
    pub fn content_rect(&self, rect: Rect) -> Rect {
        let x = if self.show_sidebar {
            self.sidebar_width
        } else {
            0.0
        };
        Rect::from_xywh(
            rect.x() + x,
            rect.y(),
            (rect.width() - x).max(0.0),
            rect.height(),
        )
    }
}

/// Widok dzielony z panelem **nakładanym** (na wąskich oknach).
#[derive(Debug, Clone, Default)]
pub struct OverlaySplitView {
    open: bool,
    sidebar_width: f64,
}

impl OverlaySplitView {
    pub fn new() -> Self {
        Self {
            open: false,
            sidebar_width: 220.0,
        }
    }

    pub fn sidebar_width(mut self, width: f64) -> Self {
        self.sidebar_width = width;
        self
    }

    pub fn is_open(&self) -> bool {
        self.open
    }

    pub fn set_open(&mut self, open: bool) {
        self.open = open;
    }

    pub fn toggle(&mut self) {
        self.open = !self.open;
    }

    /// Prostokąt panelu (przyklejony do lewej).
    pub fn sidebar_rect(&self, rect: Rect) -> Rect {
        Rect::from_xywh(rect.x(), rect.y(), self.sidebar_width, rect.height())
    }

    /// Czy punkt trafia w tło poza panelem (klik zamykający; tylko gdy otwarty).
    pub fn scrim_at(&self, rect: Rect, p: Point) -> bool {
        self.open && !self.sidebar_rect(rect).contains(p)
    }

    pub fn paint_scrim(&self, ctx: &mut PaintCtx, rect: Rect) {
        if self.open {
            let theme = *ctx.theme;
            use libluna_core::Color;
            ctx.scene.fill_rect(
                rect,
                Color::rgba(0.0, 0.0, 0.0, theme.metrics.sheet.scrim as f32),
            );
        }
    }
}

/// Panel boczny: tło + krawędź, obszar zawartości na listę ([`NavList`](crate::NavList)).
#[derive(Debug, Clone, Default)]
pub struct Sidebar;

impl Sidebar {
    pub fn new() -> Self {
        Self
    }

    /// Prostokąt zawartości (bez wewnętrznych wcięć).
    pub fn content_rect(&self, rect: Rect, theme: &Theme) -> Rect {
        rect.deflate(Insets::symmetric(theme.spacing.sm, 0.0))
    }

    /// Rysuje tło panelu i krawędź po prawej.
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
        let theme = *ctx.theme;
        ctx.scene.fill_rect(rect, theme.colors.sidebar_bg);
        ctx.scene.fill_rect(
            Rect::from_xywh(rect.right() - 1.0, rect.y(), 1.0, rect.height()),
            theme.colors.border,
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rect() -> Rect {
        Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
    }

    #[test]
    fn navigation_view_back_only_after_push() {
        let theme = Theme::light();
        let mut nav = NavigationView::new();
        assert!(nav.back_rect(rect(), &theme).is_none());
        assert!(!nav.can_pop());
        nav.push();
        assert!(nav.can_pop());
        let back = nav.back_rect(rect(), &theme).unwrap();
        assert!(nav.back_at(rect(), &theme, back.center()));
        assert!(nav.pop());
        assert!(!nav.pop());
    }

    #[test]
    fn split_view_content_and_sidebar() {
        let v = NavigationSplitView::new().sidebar_width(200.0);
        assert_eq!(v.sidebar_rect(rect()).unwrap().width(), 200.0);
        assert_eq!(v.content_rect(rect()).x(), 200.0);
        let mut v = v;
        v.set_show_sidebar(false);
        assert!(v.sidebar_rect(rect()).is_none());
        assert_eq!(v.content_rect(rect()), rect());
    }

    #[test]
    fn overlay_scrim_closes_only_when_open() {
        let mut o = OverlaySplitView::new().sidebar_width(200.0);
        assert!(!o.scrim_at(rect(), Point::new(500.0, 300.0)));
        o.set_open(true);
        assert!(o.scrim_at(rect(), Point::new(500.0, 300.0)));
        assert!(!o.scrim_at(rect(), Point::new(50.0, 300.0)));
    }
}