use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc}; use gpui::{ InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, list, prelude::FluentBuilder, px, }; use crate::{ app, component::{ font_icon::{FontIcon, font_icon}, text::text, }, util::{ self, file::{FileTreeItem, FileTreeItemKind}, }, }; #[derive(gpui::IntoElement)] pub(crate) struct FileTree { state: FileTreeState, item: Box util::file::FileTreeItem>, on_item_click: Option>, } #[derive(gpui::IntoElement)] pub(crate) struct Item { item: util::file::FileTreeItem, is_expanded: bool, on_click: Box, } #[derive(Clone)] pub(crate) struct FileTreeState(Rc>); struct FileTreeStateInner { list_state: gpui::ListState, collapsed_dirs: HashSet>, visible_items: Vec, } pub(crate) fn file_tree( state: FileTreeState, item: impl Fn(usize, &gpui::Window, &mut gpui::App) -> util::file::FileTreeItem + 'static, ) -> FileTree { FileTree { state, item: Box::new(item), on_item_click: None, } } impl FileTreeState { pub(crate) fn new() -> Self { Self(Rc::new(RefCell::new(FileTreeStateInner { list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(50.)), collapsed_dirs: HashSet::new(), visible_items: Vec::new(), }))) } pub(crate) fn reset(&self, items: &[util::file::FileTreeItem]) { let mut state = self.0.borrow_mut(); state.list_state.reset(items.len()); state.visible_items = (0..items.len()).collect::>(); } pub(crate) fn toggle_directory( &mut self, dir_path: &Arc, items: &[util::file::FileTreeItem], ) { let mut state = self.0.borrow_mut(); if !state.collapsed_dirs.remove(dir_path) { state.collapsed_dirs.insert(Arc::clone(dir_path)); } state.visible_items.clear(); let mut hidden_after_level: usize = usize::MAX; for (i, item) in items.iter().enumerate() { if item.level > hidden_after_level { continue; } hidden_after_level = usize::MAX; if item.kind == FileTreeItemKind::Directory && state.collapsed_dirs.contains(&item.full_path) { hidden_after_level = item.level; } state.visible_items.push(i); } state.list_state.reset(state.visible_items.len()); } } impl FileTree { pub(crate) fn on_item_click( mut self, f: impl Fn(&usize, &mut gpui::Window, &mut gpui::App) + 'static, ) -> Self { self.on_item_click = Some(Rc::new(f)); self } } impl gpui::RenderOnce for FileTree { fn render( self, _window: &mut gpui::Window, _cx: &mut gpui::App, ) -> impl gpui::prelude::IntoElement { let list_state = self.state.0.borrow().list_state.clone(); list(list_state, move |i, window, cx| { let state = self.state.0.borrow(); let item_index = state.visible_items[i]; let on_item_click = self.on_item_click.as_ref().map(Rc::clone); let item = (self.item)(item_index, window, cx); let item = Item { is_expanded: !state.collapsed_dirs.contains(&item.full_path), item, on_click: Box::new(move |window, cx| { if let Some(f) = &on_item_click { f(&i, window, cx); } }), }; item.into_any_element() }) .size_full() } } impl gpui::RenderOnce for Item { fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { let theme = app::current_theme(cx); div() .id(gpui::SharedString::new(self.item.full_path)) .w_full() .flex() .flex_row() .items_center() .gap_1p5() .pr_2() .py_0p5() .pl(px(8. + (8 * self.item.level) as f32)) .rounded_sm() .hover(|it| it.bg(theme.colors.surface_hover)) .on_click(move |_, window, cx| (self.on_click)(window, cx)) .when( matches!(self.item.kind, FileTreeItemKind::Directory), |it| { it.child( font_icon(if self.is_expanded { FontIcon::FolderOpen } else { FontIcon::FolderClosed }) .size_3(), ) }, ) .when(matches!(self.item.kind, FileTreeItemKind::File), |it| { it.child(font_icon(FontIcon::FileBracesCorner).size_3()) }) .child(text(gpui::SharedString::new(self.item.name)).text_sm()) } }