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

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

175 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
//! `TabView` — model stron zakładek: dodawanie, zamykanie i wybór aktywnej.
//!
//! Sam nic nie rysuje — zawartość aktywnej strony rysuje aplikacja. Do paska zakładek
//! służy [`TabView::tab_bar`], który zwraca gotowy [`TabBar`] z bieżącym wyborem.

use crate::tab_bar::{Tab, TabBar};

/// Zbiór stron (zakładek) z aktywną stroną.
#[derive(Debug, Clone, Default)]
pub struct TabView {
    tabs: Vec<Tab>,
    selected: usize,
}

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

    pub fn tabs(&self) -> &[Tab] {
        &self.tabs
    }

    pub fn len(&self) -> usize {
        self.tabs.len()
    }

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

    pub fn selected(&self) -> usize {
        self.selected
    }

    pub fn selected_tab(&self) -> Option<&Tab> {
        self.tabs.get(self.selected)
    }

    pub fn selected_title(&self) -> Option<&str> {
        self.selected_tab().map(|t| t.title.as_str())
    }

    /// Dodaje zakładkę i zwraca jej indeks (pierwsza staje się aktywna).
    pub fn add(&mut self, tab: Tab) -> usize {
        self.tabs.push(tab);
        self.tabs.len() - 1
    }

    /// Dodaje zakładkę z etykietą.
    pub fn add_titled(&mut self, title: impl Into<String>) -> usize {
        self.add(Tab::new(title))
    }

    /// Wybiera zakładkę (poza zakresem jest przycinana).
    pub fn select(&mut self, index: usize) {
        if !self.tabs.is_empty() {
            self.selected = index.min(self.tabs.len() - 1);
        }
    }

    /// Zakładka następna (z zawijaniem).
    pub fn select_next(&mut self) {
        if !self.tabs.is_empty() {
            self.selected = (self.selected + 1) % self.tabs.len();
        }
    }

    /// Zakładka poprzednia (z zawijaniem).
    pub fn select_prev(&mut self) {
        if !self.tabs.is_empty() {
            self.selected = (self.selected + self.tabs.len() - 1) % self.tabs.len();
        }
    }

    /// Zamyka zakładkę; pilnuje, żeby wybór pozostał poprawny. Zwraca `true`, jeśli zamknięto.
    pub fn close(&mut self, index: usize) -> bool {
        if index >= self.tabs.len() {
            return false;
        }
        self.tabs.remove(index);
        if self.tabs.is_empty() {
            self.selected = 0;
        } else if index < self.selected {
            self.selected -= 1;
        } else if index == self.selected {
            self.selected = self.selected.min(self.tabs.len() - 1);
        }
        true
    }

    /// Pasek zakładek do narysowania (z bieżącym wyborem).
    pub fn tab_bar(&self) -> TabBar {
        let mut bar = TabBar::with_tabs(self.tabs.clone());
        bar.set_selected(self.selected);
        bar
    }
}

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

    #[test]
    fn add_and_select() {
        let mut v = TabView::new();
        assert_eq!(v.add_titled("A"), 0);
        assert_eq!(v.add_titled("B"), 1);
        assert_eq!(v.selected(), 0);
        v.select(1);
        assert_eq!(v.selected_title(), Some("B"));
        v.select(99);
        assert_eq!(v.selected(), 1); // przycięte
    }

    #[test]
    fn next_prev_wrap() {
        let mut v = TabView::new();
        v.add_titled("A");
        v.add_titled("B");
        v.add_titled("C");
        v.select(2);
        v.select_next();
        assert_eq!(v.selected(), 0);
        v.select_prev();
        assert_eq!(v.selected(), 2);
    }

    #[test]
    fn close_before_selection_shifts_it() {
        let mut v = TabView::new();
        v.add_titled("A");
        v.add_titled("B");
        v.add_titled("C");
        v.select(2);
        assert!(v.close(0));
        assert_eq!(v.selected(), 1);
        assert_eq!(v.selected_title(), Some("C"));
    }

    #[test]
    fn close_selected_picks_neighbour() {
        let mut v = TabView::new();
        v.add_titled("A");
        v.add_titled("B");
        v.add_titled("C");
        v.select(2); // ostatnia
        v.close(2);
        assert_eq!(v.selected(), 1); // cofnięcie do sąsiada
        assert_eq!(v.selected_title(), Some("B"));
    }

    #[test]
    fn close_last_empties() {
        let mut v = TabView::new();
        v.add_titled("A");
        assert!(v.close(0));
        assert!(v.is_empty());
        assert_eq!(v.selected(), 0);
        assert_eq!(v.selected_title(), None);
        assert!(!v.close(0)); // już pusto
    }

    #[test]
    fn tab_bar_reflects_state() {
        let mut v = TabView::new();
        v.add(Tab::new("A").closable(true));
        v.add_titled("B");
        v.select(1);
        let bar = v.tab_bar();
        assert_eq!(bar.len(), 2);
        assert_eq!(bar.selected(), 1);
        assert!(bar.tabs()[0].closable);
    }
}