197 lines
5.8 KiB
Rust
197 lines
5.8 KiB
Rust
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<dyn Fn(usize, &gpui::Window, &mut gpui::App) -> util::file::FileTreeItem>,
|
|
on_item_click: Option<Rc<dyn Fn(&usize, &mut gpui::Window, &mut gpui::App)>>,
|
|
}
|
|
|
|
#[derive(gpui::IntoElement)]
|
|
pub(crate) struct Item {
|
|
item: util::file::FileTreeItem,
|
|
is_expanded: bool,
|
|
is_highlighed: bool,
|
|
on_click: Box<dyn Fn(&mut gpui::Window, &mut gpui::App) + 'static>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct FileTreeState(Rc<RefCell<FileTreeStateInner>>);
|
|
|
|
struct FileTreeStateInner {
|
|
list_state: gpui::ListState,
|
|
collapsed_dirs: HashSet<Arc<str>>,
|
|
visible_items: Vec<usize>,
|
|
highlighted_items: HashSet<usize>,
|
|
}
|
|
|
|
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(),
|
|
highlighted_items: HashSet::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::<Vec<_>>();
|
|
}
|
|
|
|
pub(crate) fn highlight_item(&mut self, index: usize) {
|
|
let mut state = self.0.borrow_mut();
|
|
state.highlighted_items.clear();
|
|
state.highlighted_items.insert(index);
|
|
}
|
|
|
|
pub(crate) fn toggle_directory(
|
|
&mut self,
|
|
dir_path: &Arc<str>,
|
|
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),
|
|
is_highlighed: state.highlighted_items.contains(&item_index),
|
|
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| {
|
|
if self.is_highlighed {
|
|
it
|
|
} else {
|
|
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())
|
|
})
|
|
.when(self.is_highlighed, |it| {
|
|
it.bg(theme.colors.accent_muted)
|
|
.text_color(theme.colors.accent_fg)
|
|
})
|
|
.child(text(gpui::SharedString::new(self.item.name)).text_sm())
|
|
}
|
|
}
|