Editor: Unity-style file explorer in the Project panel
The last item of the Stage-10 editor-UX batch. The Project panel's fixed typed-folder listing becomes a real file explorer over assets/: - breadcrumbs + double-click folder navigation, "➕ New Folder", inline rename rows, context menus (Open / Rename / Delete), drag a row onto a folder (or "..") to move it, drag files in from the OS to import into the current folder, double-click to open (scripts via the external-editor flow, others via xdg-open). - All behavior lives egui-free in editor/src/explorer.rs (listing, breadcrumbs, name validation/uniquing, create/rename/move/delete/ import) and is unit-tested; the shell only renders it. Renames and moves ride the uid-preserving AssetDatabase ops so saved AssetRefs keep resolving; unregistered files fall back to fs::rename. Folders delete only when empty — no recursive asset deletion. - Engine: AssetDatabase::scan now walks the WHOLE assets/ tree instead of just the typed folders, so assets organised into custom folders register and survive rescans (covered by updated unit tests). File operations act immediately and bypass the undo stack, like the hierarchy's structural edits. GUI piece — needs an eye-check before promotion to main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -440,18 +440,21 @@ impl AssetDatabase {
|
||||
uid
|
||||
}
|
||||
|
||||
/// Scans the typed asset folders under `assets/` and reconciles the
|
||||
/// database with what is on disk: existing files keep their uid, new files
|
||||
/// are [registered](Self::register), and entries whose files no longer exist
|
||||
/// Scans everything under `assets/` and reconciles the database with what
|
||||
/// is on disk: existing files keep their uid, new files are
|
||||
/// [registered](Self::register), and entries whose files no longer exist
|
||||
/// are dropped. Returns the number of newly registered assets.
|
||||
///
|
||||
/// The walk covers the **whole** assets tree, not just the typed folders —
|
||||
/// the editor's file explorer lets a project organise assets in arbitrary
|
||||
/// folders, and a scan must never prune them. Files outside a typed folder
|
||||
/// classify by extension (else [`Other`](AssetKind::Other)), as always.
|
||||
///
|
||||
/// Call [`save`](Self::save) afterwards to persist any new uids.
|
||||
pub fn scan(&mut self) -> usize {
|
||||
let assets_dir = self.assets_dir();
|
||||
let mut found: Vec<String> = Vec::new();
|
||||
for kind in AssetKind::TYPED {
|
||||
collect_files(&assets_dir.join(kind.folder()), &assets_dir, &mut found);
|
||||
}
|
||||
collect_files(&assets_dir, &assets_dir, &mut found);
|
||||
|
||||
// Drop entries whose backing file disappeared.
|
||||
let present: std::collections::HashSet<&String> = found.iter().collect();
|
||||
@@ -811,12 +814,27 @@ mod tests {
|
||||
assert_eq!(db.len(), 3);
|
||||
assert_eq!(db.entries_of_kind(AssetKind::Font).count(), 1);
|
||||
|
||||
// The walk covers the whole tree: a custom folder and a loose root
|
||||
// file both register (kind from extension, else Other) and survive
|
||||
// subsequent scans.
|
||||
touch_asset(&root, "props/crate.glb");
|
||||
touch_asset(&root, "notes.md");
|
||||
assert_eq!(db.scan(), 2);
|
||||
let crate_uid = db.uid_of("props/crate.glb").unwrap();
|
||||
assert_eq!(db.entry(crate_uid).unwrap().kind, AssetKind::Model);
|
||||
assert_eq!(
|
||||
db.entry(db.uid_of("notes.md").unwrap()).unwrap().kind,
|
||||
AssetKind::Other
|
||||
);
|
||||
assert_eq!(db.scan(), 0);
|
||||
assert_eq!(db.uid_of("props/crate.glb"), Some(crate_uid));
|
||||
|
||||
// Remove one file and rescan: it is pruned, the rest keep their uids.
|
||||
let font_uid = db.uid_of("fonts/Inter.ttf").unwrap();
|
||||
let wall_uid = db.uid_of("textures/wall.png").unwrap();
|
||||
std::fs::remove_file(root.join(ASSETS_DIR).join("fonts/Inter.ttf")).unwrap();
|
||||
assert_eq!(db.scan(), 0);
|
||||
assert_eq!(db.len(), 2);
|
||||
assert_eq!(db.len(), 4, "wall + cube + crate + notes remain");
|
||||
assert!(db.entry(font_uid).is_none());
|
||||
assert_eq!(db.uid_of("textures/wall.png"), Some(wall_uid));
|
||||
|
||||
@@ -922,12 +940,11 @@ mod tests {
|
||||
db.move_asset(uid, "misc/brick.dat").unwrap();
|
||||
assert_eq!(db.entry(uid).unwrap().kind, AssetKind::Other);
|
||||
|
||||
// A rescan does not disturb the moved entry's uid... (misc/ is not a
|
||||
// typed folder, so the entry survives only because scan never saw it —
|
||||
// move back first to prove the typed-folder case.)
|
||||
db.move_asset(uid, "textures/brick.png").unwrap();
|
||||
// A rescan (which walks the whole tree) does not disturb the moved
|
||||
// entry's uid, even outside the typed folders.
|
||||
db.scan();
|
||||
assert_eq!(db.uid_of("textures/brick.png"), Some(uid));
|
||||
assert_eq!(db.uid_of("misc/brick.dat"), Some(uid));
|
||||
db.move_asset(uid, "textures/brick.png").unwrap();
|
||||
|
||||
// No-op and error cases.
|
||||
db.move_asset(uid, "textures/brick.png").unwrap(); // unchanged path: Ok
|
||||
|
||||
Reference in New Issue
Block a user