feat: impl pull request file tree
This commit is contained in:
176
src/component/file_tree.rs
Normal file
176
src/component/file_tree.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
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,
|
||||
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>,
|
||||
}
|
||||
|
||||
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::<Vec<_>>();
|
||||
}
|
||||
|
||||
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),
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user