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}