basalt_tui/
vault_selector_modal.rs1use 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}