Skip to main content

basalt_tui/
vault_selector_modal.rs

1use std::marker::PhantomData;
2
3use basalt_core::obsidian::Vault;
4use ratatui::{
5    buffer::Buffer,
6    layout::{Constraint, Flex, Layout, Rect},
7    widgets::{BorderType, Clear, ScrollbarState, StatefulWidget, Widget},
8};
9
10use crate::{
11    app::Message as AppMessage,
12    config::Theme,
13    vault_selector::{VaultSelector, VaultSelectorState},
14};
15
16#[derive(Clone, Debug, PartialEq)]
17pub enum Message {
18    Toggle,
19    Up,
20    Down,
21    Select,
22    Close,
23}
24
25pub fn update<'a>(
26    message: &Message,
27    state: &mut VaultSelectorModalState<'a>,
28) -> Option<AppMessage<'a>> {
29    match message {
30        Message::Up => state.previous(),
31        Message::Down => state.next(),
32        Message::Toggle => state.toggle_visibility(),
33        Message::Close => state.hide(),
34        Message::Select => {
35            state.select();
36            if let Some(vault) = state.selected_item() {
37                state.hide();
38                return Some(AppMessage::OpenVault(vault));
39            }
40        }
41    };
42
43    None
44}
45
46#[derive(Debug, Default, Clone, PartialEq)]
47pub struct VaultSelectorModalState<'a> {
48    pub vault_selector_state: VaultSelectorState<'a>,
49    pub visible: bool,
50}
51
52impl<'a> VaultSelectorModalState<'a> {
53    pub fn new(items: Vec<&'a Vault>) -> Self {
54        Self {
55            vault_selector_state: VaultSelectorState::new(items),
56            visible: false,
57        }
58    }
59
60    pub fn selected(&self) -> Option<usize> {
61        self.vault_selector_state.selected()
62    }
63
64    pub fn select(&mut self) {
65        self.vault_selector_state.select();
66    }
67
68    pub fn selected_item(&self) -> Option<&'a Vault> {
69        self.vault_selector_state
70            .selected()
71            .and_then(|index| self.vault_selector_state.items.get(index).cloned())
72    }
73
74    pub fn get_item(self, index: usize) -> Option<&'a Vault> {
75        self.vault_selector_state.get_item(index)
76    }
77
78    pub fn next(&mut self) {
79        self.vault_selector_state.next();
80    }
81
82    pub fn previous(&mut self) {
83        self.vault_selector_state.previous();
84    }
85
86    pub fn hide(&mut self) {
87        self.visible = false;
88    }
89
90    pub fn toggle_visibility(&mut self) {
91        self.visible = !self.visible;
92    }
93}
94
95pub struct VaultSelectorModal<'a> {
96    _lifetime: PhantomData<&'a ()>,
97    pub border_type: BorderType,
98    pub vault_active: String,
99    pub theme: Theme,
100}
101
102impl<'a> VaultSelectorModal<'a> {
103    pub fn new(border_type: BorderType, vault_active: String, theme: Theme) -> Self {
104        Self {
105            _lifetime: PhantomData,
106            border_type,
107            vault_active,
108            theme,
109        }
110    }
111
112    fn modal_area(&self, area: Rect) -> Rect {
113        let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
114        let horizontal = Layout::horizontal([Constraint::Length(60)]).flex(Flex::Center);
115        let [area] = vertical.areas(area);
116        let [area] = horizontal.areas(area);
117        area
118    }
119}
120
121impl<'a> StatefulWidget for VaultSelectorModal<'a> {
122    type State = VaultSelectorModalState<'a>;
123
124    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
125    where
126        Self: Sized,
127    {
128        let area = self.modal_area(area);
129        Widget::render(Clear, area, buf);
130        VaultSelector::new(self.border_type, self.vault_active, self.theme).render(
131            area,
132            buf,
133            &mut state.vault_selector_state,
134        );
135    }
136}
137
138#[derive(Debug, Default, Clone, PartialEq)]
139pub struct ModalTitle<'a> {
140    pub left: &'a str,
141    pub right: Option<&'a str>,
142}
143
144impl<'a> ModalTitle<'a> {
145    pub fn new(title_left: &'a str, title_right: Option<&'a str>) -> Self {
146        Self {
147            left: title_left,
148            right: title_right,
149        }
150    }
151}
152
153#[derive(Debug, Default, Clone, PartialEq)]
154pub struct ModalState<'a> {
155    pub scrollbar_state: ScrollbarState,
156    pub scrollbar_position: usize,
157    pub viewport_height: usize,
158    pub text: &'a str,
159    pub title: ModalTitle<'a>,
160    pub is_open: bool,
161}
162
163impl<'a> ModalState<'a> {
164    pub fn new(title: ModalTitle<'a>, text: &'a str) -> Self {
165        Self {
166            title,
167            text,
168            scrollbar_state: ScrollbarState::new(text.lines().count()),
169            ..Default::default()
170        }
171    }
172
173    pub fn scroll_up(self, amount: usize) -> Self {
174        let scrollbar_position = self.scrollbar_position.saturating_sub(amount);
175        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
176
177        Self {
178            scrollbar_state,
179            scrollbar_position,
180            ..self
181        }
182    }
183
184    pub fn scroll_down(self, amount: usize) -> Self {
185        let scrollbar_position = self
186            .scrollbar_position
187            .saturating_add(amount)
188            .min(self.text.lines().count());
189
190        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
191
192        Self {
193            scrollbar_state,
194            scrollbar_position,
195            ..self
196        }
197    }
198
199    pub fn reset_scrollbar(self) -> Self {
200        Self {
201            scrollbar_state: ScrollbarState::default(),
202            scrollbar_position: 0,
203            ..self
204        }
205    }
206}