Skip to main content

tui_nuances/
switcher.rs

1//! [`ThemeSwitcher`]: cycle through themes in demos and settings screens.
2
3use crate::theme::Theme;
4
5/// A wrapping cursor over a list of themes.
6///
7/// ```
8/// use tui_nuances::ThemeSwitcher;
9///
10/// let mut s = ThemeSwitcher::new();
11/// let first = s.current().name.clone();
12/// s.next();
13/// assert_ne!(s.current().name, first);
14/// s.prev();
15/// assert_eq!(s.current().name, first);
16/// assert!(s.set_by_name("nord"));
17/// ```
18#[derive(Debug, Clone, PartialEq)]
19pub struct ThemeSwitcher {
20    themes: Vec<Theme>,
21    index: usize,
22}
23
24impl Default for ThemeSwitcher {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl ThemeSwitcher {
31    /// Switcher over all built-in themes, starting at the first.
32    #[must_use]
33    pub fn new() -> Self {
34        Self {
35            themes: Theme::all(),
36            index: 0,
37        }
38    }
39
40    /// Switcher over custom themes. An empty list falls back to the built-ins.
41    #[must_use]
42    pub fn with_themes(themes: Vec<Theme>) -> Self {
43        if themes.is_empty() {
44            Self::new()
45        } else {
46            Self { themes, index: 0 }
47        }
48    }
49
50    /// The active theme.
51    #[must_use]
52    pub fn current(&self) -> &Theme {
53        &self.themes[self.index]
54    }
55
56    /// Index of the active theme.
57    #[must_use]
58    pub fn index(&self) -> usize {
59        self.index
60    }
61
62    /// Number of themes (always at least 1).
63    #[must_use]
64    pub fn len(&self) -> usize {
65        self.themes.len()
66    }
67
68    /// Always `false`; a switcher holds at least one theme.
69    #[must_use]
70    pub fn is_empty(&self) -> bool {
71        false
72    }
73
74    /// All themes in order.
75    #[must_use]
76    pub fn themes(&self) -> &[Theme] {
77        &self.themes
78    }
79
80    /// Names of all themes in order.
81    #[must_use]
82    pub fn names(&self) -> Vec<&str> {
83        self.themes.iter().map(|t| t.name.as_str()).collect()
84    }
85
86    /// Advance (wrapping) and return the new active theme.
87    #[allow(clippy::should_implement_trait)]
88    pub fn next(&mut self) -> &Theme {
89        self.index = (self.index + 1) % self.themes.len();
90        self.current()
91    }
92
93    /// Step back (wrapping) and return the new active theme.
94    pub fn prev(&mut self) -> &Theme {
95        self.index = (self.index + self.themes.len() - 1) % self.themes.len();
96        self.current()
97    }
98
99    /// Jump to `index`, wrapping modulo the number of themes.
100    pub fn set_index(&mut self, index: usize) -> &Theme {
101        self.index = index % self.themes.len();
102        self.current()
103    }
104
105    /// Select a theme by name (same normalisation as [`Theme::by_name`]).
106    /// Returns `false` and keeps the selection when not found.
107    pub fn set_by_name(&mut self, name: &str) -> bool {
108        let key = name.trim().to_ascii_lowercase().replace(['_', ' '], "-");
109        match self.themes.iter().position(|t| t.name == key) {
110            Some(i) => {
111                self.index = i;
112                true
113            }
114            None => false,
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn starts_at_first() {
125        let s = ThemeSwitcher::new();
126        assert_eq!(s.index(), 0);
127        assert_eq!(s.len(), Theme::all().len());
128        assert!(!s.is_empty());
129    }
130
131    #[test]
132    fn next_wraps() {
133        let mut s = ThemeSwitcher::new();
134        for _ in 0..s.len() {
135            s.next();
136        }
137        assert_eq!(s.index(), 0);
138    }
139
140    #[test]
141    fn prev_wraps() {
142        let mut s = ThemeSwitcher::new();
143        s.prev();
144        assert_eq!(s.index(), s.len() - 1);
145    }
146
147    #[test]
148    fn set_index_wraps() {
149        let mut s = ThemeSwitcher::new();
150        s.set_index(s.len() + 2);
151        assert_eq!(s.index(), 2);
152    }
153
154    #[test]
155    fn set_by_name_found_and_missing() {
156        let mut s = ThemeSwitcher::new();
157        assert!(s.set_by_name("Tokyo Night"));
158        assert_eq!(s.current().name, "tokyo-night");
159        assert!(!s.set_by_name("zzz"));
160        assert_eq!(s.current().name, "tokyo-night");
161    }
162
163    #[test]
164    fn custom_and_empty() {
165        let s = ThemeSwitcher::with_themes(vec![Theme::default().name("a")]);
166        assert_eq!(s.names(), vec!["a"]);
167        assert_eq!(ThemeSwitcher::with_themes(vec![]).len(), Theme::all().len());
168        let mut one = s;
169        one.next();
170        assert_eq!(one.index(), 0);
171    }
172}