cadmus_core/view/navigation/providers/
directory.rs1use crate::context::Context;
2use crate::device::CURRENT_DEVICE;
3use crate::font::Fonts;
4use crate::geom::Point;
5use crate::unit::scale_by_dpi;
6use crate::view::home::directories_bar::DirectoriesBar;
7use crate::view::navigation::stack_navigation_bar::NavigationProvider;
8use crate::view::{View, SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
9use std::collections::BTreeSet;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum SourceType {
16 Filesystem,
18 Library,
20}
21
22#[derive(Debug, Clone)]
49pub struct DirectoryNavigationProvider {
50 root: PathBuf,
51 source_type: SourceType,
52}
53
54impl DirectoryNavigationProvider {
55 pub fn filesystem(root: PathBuf) -> Self {
69 Self {
70 root,
71 source_type: SourceType::Filesystem,
72 }
73 }
74
75 pub fn library(root: PathBuf) -> Self {
89 Self {
90 root,
91 source_type: SourceType::Library,
92 }
93 }
94
95 pub fn set_root(&mut self, root: PathBuf) {
97 self.root = root;
98 }
99
100 #[inline]
102 fn list_directories(&self, path: &Path, context: &Context) -> BTreeSet<PathBuf> {
103 match self.source_type {
104 SourceType::Filesystem => self.list_filesystem_dirs(path),
105 SourceType::Library => self.list_library_dirs(path, context),
106 }
107 }
108
109 #[inline]
111 fn list_filesystem_dirs(&self, path: &Path) -> BTreeSet<PathBuf> {
112 let mut dirs = BTreeSet::new();
113
114 if !path.is_dir() {
115 return dirs;
116 }
117
118 let read_dir = match fs::read_dir(path) {
119 Ok(rd) => rd,
120 Err(_) => return dirs,
121 };
122
123 for entry in read_dir.flatten() {
124 if let Ok(metadata) = entry.metadata() {
125 if metadata.is_dir() {
126 dirs.insert(entry.path());
127 }
128 }
129 }
130
131 dirs
132 }
133
134 #[inline]
136 fn list_library_dirs(&self, path: &Path, context: &Context) -> BTreeSet<PathBuf> {
137 context.library.list(path, None, true).1
138 }
139
140 #[inline]
141 fn guess_bar_size(dirs: &BTreeSet<PathBuf>) -> usize {
142 (dirs.iter().map(|dir| dir.as_os_str().len()).sum::<usize>() / 300).clamp(1, 4)
143 }
144}
145
146impl NavigationProvider for DirectoryNavigationProvider {
147 type LevelKey = PathBuf;
148 type LevelData = BTreeSet<PathBuf>;
149 type Bar = DirectoriesBar;
150
151 fn selected_leaf_key(&self, selected: &Self::LevelKey) -> Self::LevelKey {
152 selected.clone()
153 }
154
155 fn leaf_for_bar_traversal(
156 &self,
157 selected: &Self::LevelKey,
158 context: &Context,
159 ) -> Self::LevelKey {
160 let dirs = self.list_directories(selected, context);
161
162 if dirs.is_empty() && *selected != self.root {
163 selected
164 .parent()
165 .map(|p| p.to_path_buf())
166 .unwrap_or_else(|| selected.clone())
167 } else {
168 selected.clone()
169 }
170 }
171
172 fn parent(&self, current: &Self::LevelKey) -> Option<Self::LevelKey> {
173 current.parent().map(|p| p.to_path_buf())
174 }
175
176 fn is_ancestor(&self, ancestor: &Self::LevelKey, descendant: &Self::LevelKey) -> bool {
177 descendant.starts_with(ancestor)
178 }
179
180 fn is_root(&self, key: &Self::LevelKey, _context: &Context) -> bool {
181 *key == self.root
182 }
183
184 fn fetch_level_data(&self, key: &Self::LevelKey, context: &mut Context) -> Self::LevelData {
185 self.list_directories(key, context)
186 }
187
188 fn estimate_line_count(&self, _key: &Self::LevelKey, data: &Self::LevelData) -> usize {
189 Self::guess_bar_size(data)
190 }
191
192 fn create_bar(&self, rect: crate::geom::Rectangle, key: &Self::LevelKey) -> Self::Bar {
193 DirectoriesBar::new(rect, key)
194 }
195
196 fn bar_key(&self, bar: &Self::Bar) -> Self::LevelKey {
197 bar.path.clone()
198 }
199
200 fn update_bar(
201 &self,
202 bar: &mut Self::Bar,
203 data: &Self::LevelData,
204 selected: &Self::LevelKey,
205 fonts: &mut Fonts,
206 ) {
207 bar.update_content(data, Path::new(selected), fonts);
208 }
209
210 fn update_bar_selection(&self, bar: &mut Self::Bar, selected: &Self::LevelKey) {
211 bar.update_selected(Path::new(selected));
212 }
213
214 fn resize_bar_by(&self, bar: &mut Self::Bar, delta_y: i32, fonts: &mut Fonts) -> i32 {
215 let rectangle = *bar.rect();
216 let dpi = CURRENT_DEVICE.dpi;
217 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
218 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
219
220 let y_max = (rectangle.max.y + delta_y).max(rectangle.min.y + min_height);
221 let resized = y_max - rectangle.max.y;
222
223 bar.rect_mut().max.y = y_max;
224
225 let dirs = bar.dirs();
226 let path = bar.path.clone();
227 bar.update_content(&dirs, path.as_path(), fonts);
228
229 resized
230 }
231
232 fn shift_bar(&self, bar: &mut Self::Bar, delta: Point) {
233 bar.shift(delta);
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::context::test_helpers::create_test_context;
241 use std::io::Write;
242 use tempfile::TempDir;
243
244 fn create_test_directory_structure() -> TempDir {
245 let temp_dir = tempfile::tempdir().unwrap();
246 let root = temp_dir.path();
247
248 fs::create_dir(root.join("dir_a")).unwrap();
249 fs::create_dir(root.join("dir_b")).unwrap();
250 fs::create_dir(root.join("dir_c")).unwrap();
251 fs::create_dir(root.join("dir_a").join("nested")).unwrap();
252
253 let mut file = fs::File::create(root.join("file.txt")).unwrap();
254 file.write_all(b"test").unwrap();
255
256 temp_dir
257 }
258
259 #[test]
260 fn filesystem_source_lists_all_directories() {
261 let temp_dir = create_test_directory_structure();
262 let root = temp_dir.path().to_path_buf();
263 let provider = DirectoryNavigationProvider::filesystem(root.clone());
264 let context = create_test_context();
265
266 let dirs = provider.list_directories(&root, &context);
267
268 assert_eq!(dirs.len(), 3);
269 assert!(dirs.contains(&root.join("dir_a")));
270 assert!(dirs.contains(&root.join("dir_b")));
271 assert!(dirs.contains(&root.join("dir_c")));
272 }
273
274 #[test]
275 fn filesystem_source_returns_empty_for_nonexistent_path() {
276 let root = PathBuf::from("/nonexistent/path");
277 let provider = DirectoryNavigationProvider::filesystem(root.clone());
278 let context = create_test_context();
279
280 let dirs = provider.list_directories(&root, &context);
281
282 assert!(dirs.is_empty());
283 }
284
285 #[test]
286 fn filesystem_source_returns_empty_for_file() {
287 let temp_dir = create_test_directory_structure();
288 let root = temp_dir.path().to_path_buf();
289 let provider = DirectoryNavigationProvider::filesystem(root.clone());
290 let context = create_test_context();
291
292 let dirs = provider.list_directories(&root.join("file.txt"), &context);
293
294 assert!(dirs.is_empty());
295 }
296
297 #[test]
298 fn is_root_returns_true_for_root() {
299 let temp_dir = create_test_directory_structure();
300 let root = temp_dir.path().to_path_buf();
301 let provider = DirectoryNavigationProvider::filesystem(root.clone());
302
303 assert!(provider.is_root(&root, &create_test_context()));
304 }
305
306 #[test]
307 fn is_root_returns_false_for_non_root() {
308 let temp_dir = create_test_directory_structure();
309 let root = temp_dir.path().to_path_buf();
310 let provider = DirectoryNavigationProvider::filesystem(root.clone());
311
312 let subdir = root.join("dir_a");
313 assert!(!provider.is_root(&subdir, &create_test_context()));
314 }
315
316 #[test]
317 fn fetch_level_data_returns_directories() {
318 let temp_dir = create_test_directory_structure();
319 let root = temp_dir.path().to_path_buf();
320 let provider = DirectoryNavigationProvider::filesystem(root.clone());
321
322 let dirs = provider.fetch_level_data(&root, &mut create_test_context());
323
324 assert_eq!(dirs.len(), 3);
325 }
326
327 #[test]
328 fn leaf_for_bar_traversal_returns_selected_when_has_subdirs() {
329 let temp_dir = create_test_directory_structure();
330 let root = temp_dir.path().to_path_buf();
331 let provider = DirectoryNavigationProvider::filesystem(root.clone());
332
333 let selected = root.join("dir_a");
334 let result = provider.leaf_for_bar_traversal(&selected, &create_test_context());
335
336 assert_eq!(result, selected);
337 }
338
339 #[test]
340 fn leaf_for_bar_traversal_returns_parent_when_empty() {
341 let temp_dir = create_test_directory_structure();
342 let root = temp_dir.path().to_path_buf();
343 let provider = DirectoryNavigationProvider::filesystem(root.clone());
344
345 let selected = root.join("dir_a").join("nested");
346 let result = provider.leaf_for_bar_traversal(&selected, &create_test_context());
347
348 assert_eq!(result, root.join("dir_a"));
349 }
350
351 #[test]
352 fn leaf_for_bar_traversal_returns_root_when_root_is_empty() {
353 let temp_dir = tempfile::tempdir().unwrap();
354 let root = temp_dir.path().to_path_buf();
355 let provider = DirectoryNavigationProvider::filesystem(root.clone());
356
357 let result = provider.leaf_for_bar_traversal(&root, &create_test_context());
358
359 assert_eq!(result, root);
360 }
361}