Files · Explorer: a folder as a list, tiles or columns, with one path and one order
FileExplorer shows the folder at the end of a path three ways: the directory listing, a grid of tiles, or a column per folder along the path with the next folder lit. Its name, its count and a switch of views sit above one path bar that climbs. A folder opens in place; a file goes to the host, which lists each folder it goes to. A pick in an earlier column closes the columns past it, and a new deepest column scrolls into view. The directory listing takes its pick from its owner and reports it, so the pick carries across views; its path bar and its first order are shared. The gallery walks a small file system and shows the tree, the context menu, renaming in place and a drop zone in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
906e6c8ac1
commit
f694f0d413
17 files changed
+991
-41
No files matched your search
@@ -0,0 +1,120 @@
|
||||
use ely_gpui_component::{
|
||||
files::{FileGrid, FileIcon, FileItem},
|
||||
forms::{DropZone, InlineEdit},
|
||||
lists::{DirEntry, SelectableList},
|
||||
menus::{ContextMenu, Menu, MenuItem},
|
||||
primitives::IconName,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::Timestamp;
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
pub fn context_menu(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let item = |label: &'static str, icon: IconName| {
|
||||
MenuItem::new(label)
|
||||
.icon(icon)
|
||||
.on_click(move |_, _| log::info!("gallery: {label}"))
|
||||
};
|
||||
let menu = Menu::new()
|
||||
.item(item("Open", IconName::FolderOpen))
|
||||
.item(item("Rename", IconName::PenLine))
|
||||
.item(item("Duplicate", IconName::Copy))
|
||||
.separator()
|
||||
.item(item("Move to Trash", IconName::Trash2));
|
||||
section(
|
||||
"FileContextMenu",
|
||||
"What a right press on files offers, as menus::ContextMenu draws it: open, rename, duplicate, and the trash apart.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"files-context",
|
||||
div().w(px(640.)).child(
|
||||
ContextMenu::new("files-context", menu)
|
||||
.child(FileGrid::new("files-context-grid", super::entries().into_iter().take(6))),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn rename(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let name = keep(
|
||||
"files-rename",
|
||||
|| SharedString::from("Brief.md"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = name.read(cx).clone();
|
||||
section(
|
||||
"RenameInline",
|
||||
"A name edited in place, as forms::InlineEdit does it: a press or Enter edits, Enter keeps and Escape drops; the icon follows the new name.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"files-rename",
|
||||
div()
|
||||
.w(px(320.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(FileIcon::file(&now))
|
||||
.child(div().flex_1().min_w_0().child(
|
||||
InlineEdit::new("files-rename", now).on_commit(move |text, _, cx| {
|
||||
change(&name, cx, |name| *name = text.clone())
|
||||
}),
|
||||
)),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn drop_files(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let dropped = keep("files-dropped", Vec::<DirEntry>::new, window, cx);
|
||||
let now = dropped.read(cx).clone();
|
||||
let take = dropped.clone();
|
||||
let list = now
|
||||
.iter()
|
||||
.fold(SelectableList::new("files-dropped"), |list, entry| {
|
||||
let key = entry.name().clone();
|
||||
let id = SharedString::from(format!("files-dropped-{key}"));
|
||||
list.row(key, FileItem::new(id, entry.clone()).into())
|
||||
});
|
||||
section(
|
||||
"DragDropFiles",
|
||||
"Files dropped in, as forms::DropZone takes them; each lands in the list below.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"files-drop",
|
||||
div()
|
||||
.w(px(420.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
DropZone::new("files-drop")
|
||||
.multiple()
|
||||
.hint("Any kind of file")
|
||||
.on_drop(move |paths, _, cx| {
|
||||
let entries = paths.iter().map(|path| {
|
||||
let bytes = path.metadata().expect("a dropped file reads").len();
|
||||
let name = path.file_name().expect("a file name");
|
||||
DirEntry::file(
|
||||
name.to_string_lossy().to_string(),
|
||||
bytes,
|
||||
Timestamp::now(),
|
||||
)
|
||||
});
|
||||
let entries: Vec<DirEntry> = entries.collect();
|
||||
change(&take, cx, |dropped| {
|
||||
for entry in entries {
|
||||
if !dropped.iter().any(|kept| kept.name() == entry.name()) {
|
||||
dropped.push(entry);
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(list),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use ely_gpui_component::{
|
||||
files::{FileExplorer, FileView},
|
||||
lists::{DirEntry, FileTree},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{Timestamp, ToSpan};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// What the demo's folder at `path` holds, the root first.
|
||||
pub fn listing(path: &[SharedString]) -> Vec<DirEntry> {
|
||||
let now = Timestamp::now();
|
||||
let file = |name: &str, bytes: u64, hours: i64| {
|
||||
DirEntry::file(name.to_string(), bytes, now - hours.hours())
|
||||
};
|
||||
let folder = |name: &str, hours: i64| DirEntry::folder(name.to_string(), now - hours.hours());
|
||||
let joined: Vec<&str> = path.iter().map(|level| level.as_ref()).collect();
|
||||
match joined.join("/").as_str() {
|
||||
"Home" => vec![
|
||||
folder("Projects", 2),
|
||||
folder("Documents", 30),
|
||||
folder("Music", 400),
|
||||
file("Notes.txt", 1_200, 5),
|
||||
],
|
||||
"Home/Projects" => vec![
|
||||
folder("Atrium", 1),
|
||||
folder("Dunes", 90),
|
||||
file("Roadmap.md", 6_400, 20),
|
||||
],
|
||||
"Home/Projects/Atrium" => super::entries(),
|
||||
"Home/Projects/Atrium/Design" => vec![
|
||||
file("Moodboard.png", 3_200_000, 4),
|
||||
file("Palette.json", 2_100, 8),
|
||||
file("Display.ttf", 180_000, 300),
|
||||
],
|
||||
"Home/Projects/Atrium/Recordings" => vec![
|
||||
file("Walkthrough.mov", 184_000_000, 120),
|
||||
file("Voiceover.m4a", 12_000_000, 60),
|
||||
],
|
||||
"Home/Projects/Dunes" => vec![
|
||||
file("Survey.csv", 42_000, 100),
|
||||
file("Dunes.jpg", 2_800_000, 200),
|
||||
],
|
||||
"Home/Documents" => vec![
|
||||
file("Invoice.pdf", 88_000, 40),
|
||||
file("Contract.docx", 64_000, 700),
|
||||
],
|
||||
"Home/Music" => vec![
|
||||
file("Theme.mp3", 7_400_000, 800),
|
||||
file("Ambient.flac", 31_000_000, 900),
|
||||
],
|
||||
other => panic!("the demo has no folder {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the demo's explorer stands, how it shows it, and its pick.
|
||||
#[derive(Clone)]
|
||||
struct Browsing {
|
||||
path: Vec<SharedString>,
|
||||
view: FileView,
|
||||
selected: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn explorer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep(
|
||||
"files-explorer",
|
||||
|| Browsing {
|
||||
path: ["Home", "Projects", "Atrium"]
|
||||
.map(SharedString::from)
|
||||
.to_vec(),
|
||||
view: FileView::Columns,
|
||||
selected: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = state.read(cx).clone();
|
||||
let levels = (1..=now.path.len()).map(|depth| listing(&now.path[..depth]));
|
||||
let (view, go, pick) = (state.clone(), state.clone(), state);
|
||||
let explorer = FileExplorer::new("files-explorer", now.path.clone(), levels, now.view)
|
||||
.on_view(move |to, _, cx| change(&view, cx, |it| it.view = to))
|
||||
.on_navigate(move |path, _, cx| {
|
||||
change(&go, cx, |it| {
|
||||
it.path = path.to_vec();
|
||||
it.selected = None;
|
||||
})
|
||||
})
|
||||
.on_select(move |name, _, cx| change(&pick, cx, |it| it.selected = Some(name.clone())))
|
||||
.on_open(|entry, _, _| log::info!("gallery: open {}", entry.name()));
|
||||
let explorer = match now.selected {
|
||||
Some(name) => explorer.selected(name),
|
||||
None => explorer,
|
||||
};
|
||||
section(
|
||||
"FileExplorer",
|
||||
"A folder three ways: a list that sorts by column, tiles, or columns that walk the path. Its name, its count and a switch sit above the path, which climbs; a folder opens in place.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("files-explorer", div().w(px(640.)).child(explorer)))
|
||||
}
|
||||
|
||||
/// Every file of the demo under `path`, by its path below the root.
|
||||
fn files_under(path: Vec<SharedString>, out: &mut Vec<String>) {
|
||||
for entry in listing(&path) {
|
||||
let mut next = path.clone();
|
||||
next.push(entry.name().clone());
|
||||
if entry.is_folder() {
|
||||
files_under(next, out);
|
||||
} else {
|
||||
let below: Vec<&str> = next[1..].iter().map(|level| level.as_ref()).collect();
|
||||
out.push(below.join("/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tree(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let mut paths = Vec::new();
|
||||
files_under(vec!["Home".into()], &mut paths);
|
||||
section(
|
||||
"FolderTree",
|
||||
"The same folders as lists::FileTree draws them: folders first, a filter above, Enter or a double press opens a file.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"files-tree",
|
||||
div().w(px(320.)).h(px(320.)).child(
|
||||
FileTree::new("files-tree", paths)
|
||||
.size_full()
|
||||
.on_open(|path, _, _| log::info!("gallery: open {path}")),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -6,6 +6,9 @@ use ely_gpui_component::{
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{Timestamp, ToSpan};
|
||||
|
||||
mod actions;
|
||||
mod explorer;
|
||||
|
||||
use super::Page;
|
||||
use crate::{
|
||||
probe::probe,
|
||||
@@ -29,10 +32,29 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Key("down"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("grid-picked"),
|
||||
Step::DownAt("files-explorer", 515.0, 14.0),
|
||||
Step::UpAt("files-explorer", 515.0, 14.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("explorer-icons"),
|
||||
Step::DownAt("files-explorer", 48.0, 120.0),
|
||||
Step::UpAt("files-explorer", 48.0, 120.0),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("explorer-into"),
|
||||
Step::RightAt("files-context", 150.0, 50.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("context-menu"),
|
||||
Step::Key("escape"),
|
||||
Step::DownAt("files-rename", 80.0, 14.0),
|
||||
Step::UpAt("files-rename", 80.0, 14.0),
|
||||
Step::Type("Brief notes.md"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("renamed"),
|
||||
];
|
||||
|
||||
/// A project's entries, changed a few hours to a few weeks ago.
|
||||
fn entries() -> Vec<DirEntry> {
|
||||
pub fn entries() -> Vec<DirEntry> {
|
||||
let now = Timestamp::now();
|
||||
let ago = |hours: i64| now - hours.hours();
|
||||
vec![
|
||||
@@ -170,5 +192,10 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(items(window, cx))
|
||||
.child(listing(cx))
|
||||
.child(grid(window, cx))
|
||||
.child(explorer::explorer(window, cx))
|
||||
.child(explorer::tree(cx))
|
||||
.child(actions::context_menu(cx))
|
||||
.child(actions::rename(window, cx))
|
||||
.child(actions::drop_files(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user