1use std::marker::PhantomData;
2
3use basalt_core::obsidian::Note;
4use ratatui::{
5 buffer::Buffer,
6 layout::{Alignment, Constraint, Layout, Rect},
7 style::{Style, Stylize},
8 text::Line,
9 widgets::{Block, BorderType, List, ListItem, ListState, StatefulWidgetRef},
10};
11
12#[derive(Debug, Default, Clone, PartialEq)]
13pub struct SidePanelState<'a> {
14 pub(crate) title: &'a str,
15 pub(crate) selected_item_index: Option<usize>,
16 pub(crate) items: Vec<Note>,
17 pub(crate) open: bool,
18 list_state: ListState,
19}
20
21fn calculate_offset(row: usize, items_count: usize, window_height: usize) -> usize {
48 let half = window_height / 2;
49
50 if row + half > items_count.saturating_sub(1) {
51 items_count.saturating_sub(window_height)
52 } else {
53 row.saturating_sub(half)
54 }
55}
56
57impl<'a> SidePanelState<'a> {
58 pub fn new(title: &'a str, items: Vec<Note>) -> Self {
59 SidePanelState {
60 items,
61 title,
62 selected_item_index: None,
63 list_state: ListState::default().with_selected(Some(0)),
64 open: true,
65 }
66 }
67
68 pub fn open(self) -> Self {
69 Self { open: true, ..self }
70 }
71
72 pub fn close(self) -> Self {
73 Self {
74 open: false,
75 ..self
76 }
77 }
78
79 pub fn toggle(self) -> Self {
80 Self {
81 open: !self.open,
82 ..self
83 }
84 }
85
86 pub fn update_offset_mut(&mut self, window_height: usize) -> &Self {
87 if !self.items.is_empty() {
88 let idx = self.list_state.selected().unwrap_or_default();
89 let items_count = self.items.len();
90
91 let offset = calculate_offset(idx, items_count, window_height);
92
93 let list_state = &mut self.list_state;
94 *list_state.offset_mut() = offset;
95 }
96
97 self
98 }
99
100 pub fn select(&self) -> Self {
101 Self {
102 selected_item_index: self.list_state.selected(),
103 ..self.clone()
104 }
105 }
106
107 pub fn selected(&self) -> Option<usize> {
108 self.selected_item_index
109 }
110
111 pub fn is_open(&self) -> bool {
112 self.open
113 }
114
115 pub fn next(mut self) -> Self {
116 let index = self
117 .list_state
118 .selected()
119 .map(|i| (i + 1).min(self.items.len().saturating_sub(1)));
120
121 self.list_state.select(index);
122
123 Self {
124 list_state: self.list_state,
125 ..self
126 }
127 }
128
129 pub fn previous(mut self) -> Self {
130 self.list_state.select_previous();
131
132 Self {
133 list_state: self.list_state,
134 ..self
135 }
136 }
137}
138
139#[derive(Default)]
140pub struct SidePanel<'a> {
141 _lifetime: PhantomData<&'a ()>,
142}
143
144impl<'a> StatefulWidgetRef for SidePanel<'a> {
145 type State = SidePanelState<'a>;
146
147 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
148 let block = Block::bordered()
149 .border_type(BorderType::Rounded)
150 .title_style(Style::default().italic().bold());
151
152 let items: Vec<ListItem> = state
153 .items
154 .iter()
155 .enumerate()
156 .map(|(i, item)| match state.selected() {
157 Some(selected) if selected == i => ListItem::new(if state.open {
158 format!("◆ {}", item.name)
159 } else {
160 "◆".to_string()
161 }),
162 _ if state.open => ListItem::new(format!(" {}", item.name)),
163 _ => ListItem::new("◦"),
164 })
165 .collect();
166
167 let inner_area = block.inner(area);
168
169 state.update_offset_mut(inner_area.height.into());
170
171 if state.open {
172 List::new(items.to_vec())
173 .block(
174 block
175 .title(format!(" {} ", state.title))
176 .title(Line::from(" ◀ ").alignment(Alignment::Right)),
177 )
178 .highlight_style(Style::new().reversed().dark_gray())
179 .highlight_symbol(" ")
180 .render_ref(area, buf, &mut state.list_state);
181 } else {
182 let layout = Layout::horizontal([Constraint::Length(5)]).split(area);
183
184 List::new(items)
185 .block(block.title(" ▶ "))
186 .highlight_style(Style::new().reversed().dark_gray())
187 .highlight_symbol(" ")
188 .render_ref(layout[0], buf, &mut state.list_state);
189 }
190 }
191}