Skip to main content

basalt_tui/
splash_modal.rs

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