Skip to main content

basalt_tui/
theme_selector.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Style, Stylize},
5    widgets::{
6        Block, BorderType, List, ListItem, ListState, Scrollbar, ScrollbarOrientation,
7        ScrollbarState, StatefulWidget,
8    },
9};
10
11use crate::config::Theme;
12
13#[derive(Debug, Default, Clone, PartialEq)]
14pub struct ThemeSelectorState {
15    pub(crate) items: Vec<(String, Theme)>,
16    list_state: ListState,
17}
18
19impl ThemeSelectorState {
20    pub fn new(items: Vec<(String, Theme)>) -> Self {
21        Self {
22            items,
23            list_state: ListState::default().with_selected(Some(0)),
24        }
25    }
26
27    /// Highlights the entry matching `theme`, leaving the selection unchanged
28    /// when none matches.
29    pub fn select_theme(&mut self, theme: &Theme) {
30        if let Some(index) = self.items.iter().position(|(_, item)| item == theme) {
31            self.list_state.select(Some(index));
32        }
33    }
34
35    pub fn selected_theme(&self) -> Option<Theme> {
36        self.list_state
37            .selected()
38            .and_then(|index| self.items.get(index))
39            .map(|(_, theme)| *theme)
40    }
41
42    pub fn selected_name(&self) -> Option<&str> {
43        self.list_state
44            .selected()
45            .and_then(|index| self.items.get(index))
46            .map(|(name, _)| name.as_str())
47    }
48
49    pub fn next(&mut self) {
50        let index = self
51            .list_state
52            .selected()
53            .map(|i| (i + 1).min(self.items.len().saturating_sub(1)));
54        self.list_state.select(index);
55    }
56
57    pub fn previous(&mut self) {
58        self.list_state.select_previous();
59    }
60}
61
62pub struct ThemeSelector {
63    pub border_type: BorderType,
64    pub theme: Theme,
65}
66
67impl ThemeSelector {
68    pub fn new(border_type: BorderType, theme: Theme) -> Self {
69        Self { border_type, theme }
70    }
71}
72
73impl StatefulWidget for ThemeSelector {
74    type State = ThemeSelectorState;
75
76    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
77        let items: Vec<ListItem> = state
78            .items
79            .iter()
80            .map(|(name, _)| ListItem::new(format!("  {name}")))
81            .collect();
82
83        let items_count = items.len();
84
85        List::new(items)
86            .block(
87                Block::bordered()
88                    .fg(self.theme.muted)
89                    .bg(self.theme.background)
90                    .title(" Themes ")
91                    .title_style(Style::default().italic().bold())
92                    .border_type(self.border_type),
93            )
94            .fg(self.theme.text)
95            .highlight_style(Style::new().reversed().fg(self.theme.muted))
96            .highlight_symbol(" ")
97            .render(area, buf, &mut state.list_state);
98
99        // Minimum amount of items that can be rendered without the scrollbar.
100        let min_item_amount = 4;
101
102        if !area.is_empty() && items_count > min_item_amount {
103            let mut scroll_state =
104                ScrollbarState::new(items_count).position(state.list_state.selected().unwrap_or(0));
105
106            Scrollbar::new(ScrollbarOrientation::VerticalRight).render(
107                area,
108                buf,
109                &mut scroll_state,
110            );
111        }
112    }
113}