use std::path::PathBuf; use ratatui::{ crossterm::event::{Event, KeyCode}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, }; use super::Component; pub struct Tree { root: Folder, // pub selected: PathBuf, vertical_scroll: usize, vertical_scroll_state: ScrollbarState, selected_idx: usize, focused: bool, } impl Component for Tree { fn update(&mut self, mut evt: super::AppEventClient) { if evt.is_focused_and_key_pressed(KeyCode::Up, None) { evt.mark_as_handled(); if self.selected_idx > 0 { self.selected_idx -= 1; } } else if evt.is_focused_and_key_pressed(KeyCode::Down, None) { evt.mark_as_handled(); let entries = self.root.get_entries(); if self.selected_idx < entries.len() - 1 { self.selected_idx += 1; } } if evt.has_focus() { evt.register_helper("->", "Expand"); evt.register_helper("<-", "Collapse"); evt.register_helper("↑", "Up"); evt.register_helper("↓", "Down"); } let mut tmp = self.selected_idx; self.root.update(&evt, &mut tmp); self.focused = evt.has_focus(); } fn render(&mut self, f: &mut ratatui::Frame, rect: ratatui::prelude::Rect) { let block = Block::bordered() .title("Select Folder") .borders(ratatui::widgets::Borders::ALL) .border_style(ratatui::style::Style::default().fg(if self.focused { ratatui::style::Color::Cyan } else { ratatui::style::Color::Gray })); let inner_area = block.inner(rect); f.render_widget(block, rect); let entries = self.root.get_entries(); let lines: Vec = entries.iter().map(|entry| entry.get_line()).collect(); if self.selected_idx >= self.vertical_scroll + inner_area.height as usize { self.vertical_scroll = self.selected_idx - inner_area.height as usize + 1; } else if self.selected_idx < self.vertical_scroll as usize { self.vertical_scroll = self.selected_idx; } self.vertical_scroll_state = self .vertical_scroll_state .content_length(lines.len()) .position(self.vertical_scroll as usize); let paragraph = Paragraph::new(lines).scroll((self.vertical_scroll as u16, 0)); f.render_widget(paragraph, inner_area); f.render_stateful_widget( Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(Some("↑")) .end_symbol(Some("↓")), inner_area, &mut self.vertical_scroll_state, ); // todo!("Implement the Widget trait for Tree"); } } impl Tree { pub fn new(initial_path: impl Into) -> Self { let path: PathBuf = initial_path.into(); let name = path.to_string_lossy().to_string(); let mut root = Folder::new(name, path, 0); root.selected = true; Self { root, // selected: PathBuf::new(), vertical_scroll: 0, vertical_scroll_state: ScrollbarState::default(), selected_idx: 0, focused: false, } } pub fn get_selected(&self) -> PathBuf { let entries = self.root.get_entries(); if self.selected_idx < entries.len() { return entries[self.selected_idx].path.clone(); } PathBuf::new() } // pub fn show(&mut self, ui: &mut egui::Ui) { // ui.vertical(|ui| { // self.root.show(ui, &mut self.selected); // }); // } } pub struct Folder { name: String, path: PathBuf, expanded: bool, children: Option>, selected: bool, depth: usize, } impl Folder { pub fn new(name: impl Into, path: impl Into, depth: usize) -> Self { Self { name: name.into(), path: path.into(), expanded: false, selected: false, children: Default::default(), depth, } } pub fn expand(&mut self) { if self.children.is_none() { let mut children_data = vec![]; // println!("Reading dir: {:?}", self.path); std::fs::read_dir(&self.path) .map(|entries| { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { // println!( // "Found dir: {:?}, file {}, link {}", // path, // path.is_file(), // path.is_symlink() // ); let path2 = path.clone(); let name = path2.file_name().unwrap().to_string_lossy(); children_data.push(Folder::new(name, path, self.depth + 1)); } } }) .unwrap_or_else(|err| { eprintln!("Failed to read dir: {}", err); }); children_data.sort_by_key(|folder| folder.name.clone()); self.children.replace(children_data); } } pub fn update(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) { if *selected_idx == 0 { self.selected = true; *selected_idx = usize::MAX; } else { self.selected = false; } *selected_idx -= 1; if evt.has_focus() && self.selected { if let Some(evt) = &evt.event { if let Event::Key(key) = evt { match key.code { KeyCode::Right | KeyCode::Enter => { self.expand(); self.expanded = true; } KeyCode::Left => { self.expanded = false; } _ => {} } } } } if self.expanded { if let Some(children) = &mut self.children { for child in children { child.update(evt, selected_idx); // if child.selected { // self.selected = true; // } } } } } pub fn get_entries(&self) -> Vec<&Folder> { let mut entries: Vec<&Folder> = vec![]; if self.expanded { if let Some(children) = &self.children { for child in children { entries.extend(child.get_entries()); } } } let mut res: Vec<&Folder> = vec![self]; res.extend(entries); res } pub fn get_line(&self) -> Line { let pad = " ".repeat(self.depth); Line::from(vec![Span::styled( format!( "{}{} {}", pad, if self.expanded { "▼" } else { "▶" }, self.name ), if self.selected { Style::new().bg(Color::White).fg(Color::Black) } else { Style::default() }, )]) } // pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) { // let Self { // name, // path, // expanded, // children, // } = self; // if children.is_none() { // let mut children_data = vec![]; // println!("Reading dir: {:?}", path); // std::fs::read_dir(&path) // .map(|entries| { // for entry in entries.flatten() { // let path = entry.path(); // if path.is_dir() { // println!( // "Found dir: {:?}, file {}, link {}", // path, // path.is_file(), // path.is_symlink() // ); // let path2 = path.clone(); // let name = path2.file_name().unwrap().to_string_lossy(); // children_data.push(Folder::new(name, path)); // } // } // }) // .unwrap_or_else(|err| { // eprintln!("Failed to read dir: {}", err); // }); // children_data.sort_by_key(|folder| folder.name.clone()); // children.replace(children_data); // } // ui.horizontal(|ui| { // if let Some(children) = children { // if !children.is_empty() { // let label = if *expanded { "▼" } else { "▶" }; // let response = ui.selectable_label(false, label); // if response.clicked() && !children.is_empty() { // *expanded = !*expanded; // } // } // } else { // ui.label("⌛"); // } // if ui // .selectable_label(*selected == *path, name.to_owned()) // .clicked() // { // *selected = path.clone(); // } // }); // if let Some(children) = children { // if *expanded { // ui.indent("", |ui| { // for child in children { // child.show(ui, selected); // } // }); // } // } // } }