1use ratatui::layout::Rect;
2
3use super::{App, Position};
4use crate::tree::{FileId, Kind, NodeId, Tree};
5
6const TREE_WIDTH: u16 = 32;
7const MIN_WIDTH: u16 = TREE_WIDTH + 1 + 64;
8
9#[derive(Default, Clone, Copy)]
10pub enum SidebarPreference {
11 #[default]
12 Auto,
13 Show,
14 Hide,
15}
16
17#[derive(Default, Clone, Copy, PartialEq, Eq)]
18pub(crate) enum Focus {
19 #[default]
20 Diff,
21 Files,
22}
23
24#[derive(Default)]
25pub(crate) struct Sidebar {
26 pub tree: Tree,
27 pub preference: SidebarPreference,
28 pub focus: Focus,
29 pub cursor: Option<NodeId>,
30 pub top: usize,
31 pub current: Option<FileId>,
32 pub screen: Rect,
33 pub area: Rect,
34 pub diff: Rect,
35 pub notice: Option<&'static str>,
36}
37
38impl Sidebar {
39 pub fn focused(&self) -> bool {
40 self.focus == Focus::Files
41 }
42 pub fn page(&self) -> usize {
43 usize::from(self.area.height.saturating_sub(1)).max(1)
44 }
45
46 pub fn keep_visible(&mut self) {
47 let page = self.page();
48 if let Some(position) = self.cursor.and_then(|id| self.tree.position(id)) {
49 self.top = self
50 .top
51 .min(position)
52 .max(position.saturating_sub(page - 1));
53 }
54 self.top = self.top.min(self.tree.rows().len().saturating_sub(page));
55 }
56
57 pub fn move_cursor(&mut self, amount: isize) {
58 let position = self
59 .cursor
60 .and_then(|id| self.tree.position(id))
61 .unwrap_or(0);
62 let target = position
63 .saturating_add_signed(amount)
64 .min(self.tree.rows().len().saturating_sub(1));
65 self.cursor = self.tree.rows().get(target).map(|r| r.node);
66 self.keep_visible();
67 }
68
69 pub fn scroll(&mut self, amount: isize) {
70 let page = self.page();
71 self.top = self
72 .top
73 .saturating_add_signed(amount)
74 .min(self.tree.rows().len().saturating_sub(page));
75 if let Some(position) = self.cursor.and_then(|id| self.tree.position(id)) {
76 let target = position.clamp(self.top, self.top + page - 1);
77 self.cursor = self.tree.rows().get(target).map(|r| r.node);
78 }
79 }
80
81 pub fn toggle(&mut self, id: NodeId) {
82 if let Kind::Folder { expanded, .. } = self.tree.node(id).kind {
83 self.tree.set_expanded(id, !expanded);
84 self.tree.refresh();
85 self.keep_visible();
86 }
87 }
88
89 pub fn right(&mut self) {
90 let Some(id) = self.cursor else {
91 return;
92 };
93 if let Kind::Folder { expanded, .. } = self.tree.node(id).kind {
94 if expanded {
95 self.move_cursor(1);
96 } else {
97 self.toggle(id);
98 }
99 }
100 }
101
102 pub fn left(&mut self) {
103 let Some(id) = self.cursor else {
104 return;
105 };
106 if matches!(self.tree.node(id).kind, Kind::Folder { expanded: true, .. }) {
107 self.toggle(id);
108 } else if let Some(parent) = self.tree.parent(id) {
109 self.cursor = Some(parent);
110 self.keep_visible();
111 }
112 }
113}
114
115impl App {
116 pub fn set_sidebar_preference(&mut self, preference: SidebarPreference) {
117 self.sidebar.preference = preference;
118 }
119
120 pub(crate) fn prepare_view(&mut self, screen: Rect) -> anyhow::Result<()> {
121 for file in &self.doc.files[self.sidebar.tree.len()..] {
122 self.sidebar.tree.append(file)?;
123 }
124 let current = self.row(self.cursor.row).map(|row| FileId(row.file));
125 if current != self.sidebar.current {
126 self.sidebar.current = current;
127 if self.sidebar.focus == Focus::Diff
128 && let Some(node) = current.and_then(|id| self.sidebar.tree.file_node(id))
129 {
130 self.sidebar.tree.reveal(node);
131 self.sidebar.cursor = Some(node);
132 }
133 }
134 self.sidebar.tree.refresh();
135 if self.sidebar.cursor.is_none() {
136 self.sidebar.cursor = self.sidebar.tree.rows().first().map(|r| r.node);
137 }
138 let wants_tree = match self.sidebar.preference {
139 SidebarPreference::Auto => self.doc.files.len() >= 2,
140 SidebarPreference::Show => true,
141 SidebarPreference::Hide => false,
142 };
143 let shown = wants_tree && screen.width >= MIN_WIDTH && screen.height >= 10;
144 if shown {
145 self.sidebar.notice = None;
146 }
147 self.sidebar.screen = screen;
148 self.sidebar.area = if shown {
149 Rect::new(screen.x, screen.y + 1, TREE_WIDTH, screen.height - 2)
150 } else {
151 Rect::default()
152 };
153 let inset = if shown { TREE_WIDTH + 1 } else { 0 };
154 self.sidebar.diff = if screen.width >= 40 && screen.height >= 10 {
155 Rect::new(
156 screen.x + inset,
157 screen.y + 1,
158 screen.width - inset,
159 screen.height - 2,
160 )
161 } else {
162 Rect::default()
163 };
164 if !shown {
165 self.sidebar.focus = Focus::Diff;
166 }
167 self.sidebar.keep_visible();
168 if self.sidebar.diff.is_empty() {
169 self.visible.clear();
170 return Ok(());
171 }
172 let changed = self.width != self.sidebar.diff.width || self.height != screen.height;
173 if changed {
174 self.reflow(self.layout, self.sidebar.diff.width, screen.height)?;
175 }
176 Ok(())
177 }
178
179 pub(crate) fn files_command(&mut self) -> anyhow::Result<()> {
180 if self.sidebar.focused() {
181 self.sidebar.preference = SidebarPreference::Hide;
182 self.sidebar.focus = Focus::Diff;
183 } else {
184 self.sidebar.preference = SidebarPreference::Show;
185 if self.sidebar.screen.width < MIN_WIDTH || self.sidebar.screen.height < 10 {
186 self.sidebar.notice = Some("Files need at least 97 columns × 10 rows");
187 } else {
188 self.sidebar.focus = Focus::Files;
189 if let Some(id) = self
190 .row(self.cursor.row)
191 .and_then(|r| self.sidebar.tree.file_node(FileId(r.file)))
192 {
193 self.sidebar.tree.reveal(id);
194 self.sidebar.cursor = Some(id);
195 }
196 }
197 }
198 self.prepare_view(self.sidebar.screen)?;
199 Ok(())
200 }
201
202 pub(crate) fn activate_tree(&mut self) -> bool {
203 let Some(id) = self.sidebar.cursor else {
204 return false;
205 };
206 if let Some(file) = self.sidebar.tree.node(id).file() {
207 self.cursor = Position {
208 row: self.view.file_row(&self.doc, file.0, self.split()),
209 part: 0,
210 };
211 self.top = self.cursor;
212 self.reading_anchor = None;
213 self.sidebar.focus = Focus::Diff;
214 true
215 } else {
216 self.sidebar.toggle(id);
217 false
218 }
219 }
220}