Skip to main content

diffs_rs/app/
sidebar.rs

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}