gnostr_filetreelist/
filetree.rs

1use std::{collections::BTreeSet, path::Path, usize};
2
3use crate::{
4	TreeItemInfo, error::Result, filetreeitems::FileTreeItems,
5	tree_iter::TreeIterator,
6};
7
8///
9#[derive(Copy, Clone, Debug)]
10pub enum MoveSelection {
11	Up,
12	Down,
13	Left,
14	Right,
15	Top,
16	End,
17	PageDown,
18	PageUp,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct VisualSelection {
23	pub count: usize,
24	pub index: usize,
25}
26
27/// wraps `FileTreeItems` as a datastore and adds selection
28/// functionality
29#[derive(Default)]
30pub struct FileTree {
31	items: FileTreeItems,
32	selection: Option<usize>,
33	// caches the absolute selection translated to visual index
34	visual_selection: Option<VisualSelection>,
35}
36
37impl FileTree {
38	///
39	pub fn new(
40		list: &[&Path],
41		collapsed: &BTreeSet<&String>,
42	) -> Result<Self> {
43		let mut new_self = Self {
44			items: FileTreeItems::new(list, collapsed)?,
45			selection: if list.is_empty() { None } else { Some(0) },
46			visual_selection: None,
47		};
48		new_self.visual_selection = new_self.calc_visual_selection();
49
50		Ok(new_self)
51	}
52
53	///
54	pub const fn is_empty(&self) -> bool {
55		self.items.file_count() == 0
56	}
57
58	///
59	pub const fn selection(&self) -> Option<usize> {
60		self.selection
61	}
62
63	///
64	pub fn collapse_but_root(&mut self) {
65		if !self.is_empty() {
66			self.items.collapse(0, true);
67			self.items.expand(0, false);
68		}
69	}
70
71	/// iterates visible elements starting from `start_index_visual`
72	pub fn iterate(
73		&self,
74		start_index_visual: usize,
75		max_amount: usize,
76	) -> TreeIterator<'_> {
77		let start = self
78			.visual_index_to_absolute(start_index_visual)
79			.unwrap_or_default();
80		TreeIterator::new(
81			self.items.iterate(start, max_amount),
82			self.selection,
83		)
84	}
85
86	///
87	pub const fn visual_selection(&self) -> Option<&VisualSelection> {
88		self.visual_selection.as_ref()
89	}
90
91	///
92	pub fn selected_file(&self) -> Option<&TreeItemInfo> {
93		self.selection.and_then(|index| {
94			let item = &self.items.tree_items[index];
95			if item.kind().is_path() {
96				None
97			} else {
98				Some(item.info())
99			}
100		})
101	}
102
103	///
104	pub fn collapse_recursive(&mut self) {
105		if let Some(selection) = self.selection {
106			self.items.collapse(selection, true);
107		}
108	}
109
110	///
111	pub fn expand_recursive(&mut self) {
112		if let Some(selection) = self.selection {
113			self.items.expand(selection, true);
114		}
115	}
116
117	///
118	pub fn move_selection(&mut self, dir: MoveSelection) -> bool {
119		        self.selection.is_some_and(|selection| {
120		            let new_index = match dir {
121		                MoveSelection::Up => {
122		                    self.selection_updown(selection, true)
123		                }
124		                MoveSelection::Down => {
125		                    self.selection_updown(selection, false)
126		                }
127		                MoveSelection::Left => self.selection_left(selection),
128		                MoveSelection::Right => {
129		                    self.selection_right(selection)
130		                }
131		                MoveSelection::Top => {
132		                    Self::selection_start(selection)
133		                }
134		                MoveSelection::End => self.selection_end(selection),
135		                MoveSelection::PageDown | MoveSelection::PageUp => {
136		                    None
137		                }
138		            };
139		
140		            let changed_index =
141		                new_index.is_some_and(|i| i != selection);
142		
143		            if changed_index {
144		                self.selection = new_index;
145		                self.visual_selection = self.calc_visual_selection();
146		            }
147		
148		            changed_index || new_index.is_some()
149		        })	}
150
151	pub fn select_file(&mut self, path: &Path) -> bool {
152		let new_selection = self
153			.items
154			.tree_items
155			.iter()
156			.position(|item| item.info().full_path() == path);
157
158		if new_selection == self.selection {
159			return false;
160		}
161
162		self.selection = new_selection;
163		if let Some(selection) = self.selection {
164			self.items.show_element(selection);
165		}
166		self.visual_selection = self.calc_visual_selection();
167		true
168	}
169
170	fn visual_index_to_absolute(
171		&self,
172		visual_index: usize,
173	) -> Option<usize> {
174		self.items
175			.iterate(0, self.items.len())
176			.enumerate()
177			.find_map(|(i, (abs, _))| {
178				if i == visual_index { Some(abs) } else { None }
179			})
180	}
181
182	fn calc_visual_selection(&self) -> Option<VisualSelection> {
183		self.selection.map(|selection_absolute| {
184			let mut count = 0;
185			let mut visual_index = 0;
186			for (index, _item) in
187				self.items.iterate(0, self.items.len())
188			{
189				if selection_absolute == index {
190					visual_index = count;
191				}
192
193				count += 1;
194			}
195
196			VisualSelection {
197				index: visual_index,
198				count,
199			}
200		})
201	}
202
203	const fn selection_start(current_index: usize) -> Option<usize> {
204		if current_index == 0 { None } else { Some(0) }
205	}
206
207	fn selection_end(&self, current_index: usize) -> Option<usize> {
208		let items_max = self.items.len().saturating_sub(1);
209
210		let mut new_index = items_max;
211
212		loop {
213			if self.is_visible_index(new_index) {
214				break;
215			}
216
217			if new_index == 0 {
218				break;
219			}
220
221			new_index = new_index.saturating_sub(1);
222			new_index = std::cmp::min(new_index, items_max);
223		}
224
225		if new_index == current_index {
226			None
227		} else {
228			Some(new_index)
229		}
230	}
231
232	fn selection_updown(
233		&self,
234		current_index: usize,
235		up: bool,
236	) -> Option<usize> {
237		let mut index = current_index;
238
239		loop {
240			index = {
241				let new_index = if up {
242					index.saturating_sub(1)
243				} else {
244					index.saturating_add(1)
245				};
246
247				// when reaching usize bounds
248				if new_index == index {
249					break;
250				}
251
252				if new_index >= self.items.len() {
253					break;
254				}
255
256				new_index
257			};
258
259			if self.is_visible_index(index) {
260				break;
261			}
262		}
263
264		if index == current_index {
265			None
266		} else {
267			Some(index)
268		}
269	}
270
271	fn select_parent(
272		&mut self,
273		current_index: usize,
274	) -> Option<usize> {
275		let indent =
276			self.items.tree_items[current_index].info().indent();
277
278		let mut index = current_index;
279
280		while let Some(selection) = self.selection_updown(index, true)
281		{
282			index = selection;
283
284			if self.items.tree_items[index].info().indent() < indent {
285				break;
286			}
287		}
288
289		if index == current_index {
290			None
291		} else {
292			Some(index)
293		}
294	}
295
296	fn selection_left(
297		&mut self,
298		current_index: usize,
299	) -> Option<usize> {
300		let item = &mut self.items.tree_items[current_index];
301
302		if item.kind().is_path() && !item.kind().is_path_collapsed() {
303			self.items.collapse(current_index, false);
304			return Some(current_index);
305		}
306
307		self.select_parent(current_index)
308	}
309
310	fn selection_right(
311		&mut self,
312		current_selection: usize,
313	) -> Option<usize> {
314		let item = &mut self.items.tree_items[current_selection];
315
316		if item.kind().is_path() {
317			if item.kind().is_path_collapsed() {
318				self.items.expand(current_selection, false);
319				return Some(current_selection);
320			}
321			return self.selection_updown(current_selection, false);
322		}
323
324		None
325	}
326
327	fn is_visible_index(&self, index: usize) -> bool {
328		self.items
329			.tree_items
330			.get(index)
331			.is_some_and(|item| item.info().is_visible())
332	}
333}
334
335#[cfg(test)]
336mod test {
337	use std::{collections::BTreeSet, path::Path};
338
339	use pretty_assertions::assert_eq;
340
341	use crate::{FileTree, MoveSelection};
342
343	#[test]
344	fn test_selection() {
345		let items = vec![
346			Path::new("a/b"), //
347		];
348
349		let mut tree =
350			FileTree::new(&items, &BTreeSet::new()).unwrap();
351
352		assert!(tree.move_selection(MoveSelection::Down));
353
354		assert_eq!(tree.selection, Some(1));
355
356		assert!(!tree.move_selection(MoveSelection::Down));
357
358		assert_eq!(tree.selection, Some(1));
359	}
360
361	#[test]
362	fn test_selection_skips_collapsed() {
363		let items = vec![
364			Path::new("a/b/c"), //
365			Path::new("a/d"),   //
366		];
367
368		//0 a/
369		//1   b/
370		//2     c
371		//3   d
372
373		let mut tree =
374			FileTree::new(&items, &BTreeSet::new()).unwrap();
375
376		tree.items.collapse(1, false);
377		tree.selection = Some(1);
378
379		assert!(tree.move_selection(MoveSelection::Down));
380
381		assert_eq!(tree.selection, Some(3));
382	}
383
384	#[test]
385	fn test_selection_left_collapse() {
386		let items = vec![
387			Path::new("a/b/c"), //
388			Path::new("a/d"),   //
389		];
390
391		//0 a/
392		//1   b/
393		//2     c
394		//3   d
395
396		let mut tree =
397			FileTree::new(&items, &BTreeSet::new()).unwrap();
398
399		tree.selection = Some(1);
400
401		//collapses 1
402		assert!(tree.move_selection(MoveSelection::Left));
403		// index will not change
404		assert_eq!(tree.selection, Some(1));
405
406		assert!(tree.items.tree_items[1].kind().is_path_collapsed());
407		assert!(!tree.items.tree_items[2].info().is_visible());
408	}
409
410	#[test]
411	fn test_selection_left_parent() {
412		let items = vec![
413			Path::new("a/b/c"), //
414			Path::new("a/d"),   //
415		];
416
417		//0 a/
418		//1   b/
419		//2     c
420		//3   d
421
422		let mut tree =
423			FileTree::new(&items, &BTreeSet::new()).unwrap();
424
425		tree.selection = Some(2);
426
427		assert!(tree.move_selection(MoveSelection::Left));
428		assert_eq!(tree.selection, Some(1));
429
430		assert!(tree.move_selection(MoveSelection::Left));
431		assert_eq!(tree.selection, Some(1));
432
433		assert!(tree.move_selection(MoveSelection::Left));
434		assert_eq!(tree.selection, Some(0));
435	}
436
437	#[test]
438	fn test_selection_right_expand() {
439		let items = vec![
440			Path::new("a/b/c"), //
441			Path::new("a/d"),   //
442		];
443
444		//0 a/
445		//1   b/
446		//2     c
447		//3   d
448
449		let mut tree =
450			FileTree::new(&items, &BTreeSet::new()).unwrap();
451
452		tree.items.collapse(1, false);
453		tree.items.collapse(0, false);
454		tree.selection = Some(0);
455
456		assert!(tree.move_selection(MoveSelection::Right));
457		assert_eq!(tree.selection, Some(0));
458		assert!(!tree.items.tree_items[0].kind().is_path_collapsed());
459
460		assert!(tree.move_selection(MoveSelection::Right));
461		assert_eq!(tree.selection, Some(1));
462		assert!(tree.items.tree_items[1].kind().is_path_collapsed());
463
464		assert!(tree.move_selection(MoveSelection::Right));
465		assert_eq!(tree.selection, Some(1));
466		assert!(!tree.items.tree_items[1].kind().is_path_collapsed());
467	}
468
469	#[test]
470	fn test_selection_top() {
471		let items = vec![
472			Path::new("a/b/c"), //
473			Path::new("a/d"),   //
474		];
475
476		//0 a/
477		//1   b/
478		//2     c
479		//3   d
480
481		let mut tree =
482			FileTree::new(&items, &BTreeSet::new()).unwrap();
483
484		tree.selection = Some(3);
485
486		assert!(tree.move_selection(MoveSelection::Top));
487		assert_eq!(tree.selection, Some(0));
488	}
489
490	#[test]
491	fn test_visible_selection() {
492		let items = vec![
493			Path::new("a/b/c"),  //
494			Path::new("a/b/c2"), //
495			Path::new("a/d"),    //
496		];
497
498		//0 a/
499		//1   b/
500		//2     c
501		//3     c2
502		//4   d
503
504		let mut tree =
505			FileTree::new(&items, &BTreeSet::new()).unwrap();
506
507		tree.selection = Some(1);
508		assert!(tree.move_selection(MoveSelection::Left));
509		assert!(tree.move_selection(MoveSelection::Down));
510		let s = tree.visual_selection().unwrap();
511
512		assert_eq!(s.count, 3);
513		assert_eq!(s.index, 2);
514	}
515}