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