Files
novem/src/component/file_tree.rs

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())
}
}