cadmus_core/view/navigation/providers/
directory.rs

1use 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/// The source of directory listings.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum SourceType {
16    /// Plain filesystem - shows all directories.
17    Filesystem,
18    /// Library-aware - respects library filtering rules.
19    Library,
20}
21
22/// A navigation provider that traverses directory structures.
23///
24/// This provider can use either plain filesystem operations or library-aware
25/// filtering, depending on the use case:
26/// - Filesystem mode: Shows all directories (for FileChooser)
27/// - Library mode: Respects library settings like hidden file filtering (for Home view)
28///
29/// # Examples
30///
31/// Create a filesystem provider for browsing all directories:
32///
33/// ```
34/// use std::path::PathBuf;
35/// use cadmus_core::view::navigation::providers::directory::DirectoryNavigationProvider;
36///
37/// let provider = DirectoryNavigationProvider::filesystem(PathBuf::from("/home/user"));
38/// ```
39///
40/// Create a library provider for filtered navigation:
41///
42/// ```
43/// use std::path::PathBuf;
44/// use cadmus_core::view::navigation::providers::directory::DirectoryNavigationProvider;
45///
46/// let provider = DirectoryNavigationProvider::library(PathBuf::from("/home/user/books"));
47/// ```
48#[derive(Debug, Clone)]
49pub struct DirectoryNavigationProvider {
50    root: PathBuf,
51    source_type: SourceType,
52}
53
54impl DirectoryNavigationProvider {
55    /// Creates a provider for plain filesystem navigation.
56    ///
57    /// This shows all directories without any filtering, suitable for
58    /// file chooser dialogs that need to browse the entire filesystem.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use std::path::PathBuf;
64    /// use cadmus_core::view::navigation::providers::directory::DirectoryNavigationProvider;
65    ///
66    /// let provider = DirectoryNavigationProvider::filesystem(PathBuf::from("/home/user"));
67    /// ```
68    pub fn filesystem(root: PathBuf) -> Self {
69        Self {
70            root,
71            source_type: SourceType::Filesystem,
72        }
73    }
74
75    /// Creates a provider for library-aware navigation.
76    ///
77    /// This respects library settings like hidden file filtering,
78    /// suitable for the Home view navigation.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use std::path::PathBuf;
84    /// use cadmus_core::view::navigation::providers::directory::DirectoryNavigationProvider;
85    ///
86    /// let provider = DirectoryNavigationProvider::library(PathBuf::from("/home/user/books"));
87    /// ```
88    pub fn library(root: PathBuf) -> Self {
89        Self {
90            root,
91            source_type: SourceType::Library,
92        }
93    }
94
95    /// Updates the root directory for this provider.
96    pub fn set_root(&mut self, root: PathBuf) {
97        self.root = root;
98    }
99
100    /// Lists directories using the configured source.
101    #[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    /// Lists directories from the filesystem without filtering.
110    #[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    /// Lists directories using the library's filtering rules.
135    #[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}