docs: add asset browser implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
259
docs/superpowers/plans/2026-03-26-asset-browser.md
Normal file
259
docs/superpowers/plans/2026-03-26-asset-browser.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Asset Browser Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a file system asset browser panel to the editor that displays files/folders in a flat list with navigation.
|
||||
|
||||
**Architecture:** `AssetBrowser` struct manages directory state (current path, entries cache). `asset_browser_panel` function renders the UI. Uses `std::fs::read_dir` for file listing. No external dependencies needed.
|
||||
|
||||
**Tech Stack:** Rust, std::fs, voltex_editor (UiContext, Rect, LayoutState, widgets)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-26-asset-browser-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: AssetBrowser struct + logic + tests
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/voltex_editor/src/asset_browser.rs`
|
||||
- Modify: `crates/voltex_editor/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write tests and implementation**
|
||||
|
||||
Create `crates/voltex_editor/src/asset_browser.rs` with the full AssetBrowser implementation and tests. The struct handles directory scanning, navigation, and path safety.
|
||||
|
||||
Key implementation details:
|
||||
- `new(root)` → canonicalize root, set current=root, refresh
|
||||
- `refresh()` → read_dir, collect into Vec<DirEntry>, sort (dirs first, then by name)
|
||||
- `navigate_to(dir_name)` → join path, check starts_with(root) && is_dir, refresh
|
||||
- `go_up()` → parent path, check >= root, refresh
|
||||
- `relative_path()` → strip_prefix(root) display
|
||||
- `format_size(bytes)` → "B" / "KB" / "MB" formatting
|
||||
|
||||
Tests using `std::fs` temp directories:
|
||||
- test_new_scans_entries
|
||||
- test_navigate_to
|
||||
- test_go_up
|
||||
- test_go_up_at_root
|
||||
- test_root_guard
|
||||
- test_entries_sorted (dirs before files)
|
||||
- test_format_size
|
||||
|
||||
- [ ] **Step 2: Add module to lib.rs**
|
||||
|
||||
```rust
|
||||
pub mod asset_browser;
|
||||
pub use asset_browser::{AssetBrowser, asset_browser_panel};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
Run: `cargo test -p voltex_editor --lib asset_browser -- --nocapture`
|
||||
Expected: all tests PASS
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/voltex_editor/src/asset_browser.rs crates/voltex_editor/src/lib.rs
|
||||
git commit -m "feat(editor): add AssetBrowser with directory navigation and file listing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: asset_browser_panel UI function
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/voltex_editor/src/asset_browser.rs`
|
||||
|
||||
- [ ] **Step 1: Write test**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn test_panel_draws_commands() {
|
||||
let dir = std::env::temp_dir().join("voltex_ab_ui_test");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
std::fs::write(dir.join("test.txt"), "hello").unwrap();
|
||||
|
||||
let mut browser = AssetBrowser::new(dir.clone());
|
||||
let mut ui = UiContext::new(800.0, 600.0);
|
||||
let rect = Rect { x: 0.0, y: 0.0, w: 300.0, h: 400.0 };
|
||||
|
||||
ui.begin_frame(0.0, 0.0, false);
|
||||
asset_browser_panel(&mut ui, &mut browser, &rect);
|
||||
ui.end_frame();
|
||||
|
||||
assert!(ui.draw_list.commands.len() > 0);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement asset_browser_panel**
|
||||
|
||||
```rust
|
||||
pub fn asset_browser_panel(
|
||||
ui: &mut UiContext,
|
||||
browser: &mut AssetBrowser,
|
||||
rect: &Rect,
|
||||
) {
|
||||
ui.layout = LayoutState::new(rect.x + PADDING, rect.y + PADDING);
|
||||
|
||||
// Current path
|
||||
let path_text = format!("Path: /{}", browser.relative_path());
|
||||
ui.text(&path_text);
|
||||
|
||||
// Go up button (not at root)
|
||||
if browser.current != browser.root {
|
||||
if ui.button("[..]") {
|
||||
browser.go_up();
|
||||
return; // entries changed, redraw next frame
|
||||
}
|
||||
}
|
||||
|
||||
// Entry list
|
||||
let gw = ui.font.glyph_width as f32;
|
||||
let gh = ui.font.glyph_height as f32;
|
||||
let line_h = gh + PADDING;
|
||||
let mut clicked_dir: Option<String> = None;
|
||||
|
||||
for entry in browser.entries() {
|
||||
let y = ui.layout.cursor_y;
|
||||
let x = rect.x + PADDING;
|
||||
|
||||
// Highlight selected file
|
||||
if !entry.is_dir {
|
||||
if browser.selected_file.as_deref() == Some(&entry.name) {
|
||||
ui.draw_list.add_rect(rect.x, y, rect.w, line_h, COLOR_SELECTED);
|
||||
}
|
||||
}
|
||||
|
||||
// Click detection
|
||||
if ui.mouse_clicked && ui.mouse_in_rect(rect.x, y, rect.w, line_h) {
|
||||
if entry.is_dir {
|
||||
clicked_dir = Some(entry.name.clone());
|
||||
} else {
|
||||
browser.selected_file = Some(entry.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Draw label
|
||||
let label = if entry.is_dir {
|
||||
format!("[D] {}", entry.name)
|
||||
} else {
|
||||
format!(" {}", entry.name)
|
||||
};
|
||||
|
||||
let text_y = y + (line_h - gh) * 0.5;
|
||||
let mut cx = x;
|
||||
for ch in label.chars() {
|
||||
let (u0, v0, u1, v1) = ui.font.glyph_uv(ch);
|
||||
ui.draw_list.add_rect_uv(cx, text_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
|
||||
cx += gw;
|
||||
}
|
||||
|
||||
ui.layout.cursor_y += line_h;
|
||||
}
|
||||
|
||||
// Navigate after iteration (avoids borrow conflict)
|
||||
if let Some(dir) = clicked_dir {
|
||||
browser.navigate_to(&dir);
|
||||
}
|
||||
|
||||
// Selected file info
|
||||
if let Some(ref file_name) = browser.selected_file.clone() {
|
||||
ui.text("-- File Info --");
|
||||
ui.text(&format!("Name: {}", file_name));
|
||||
if let Some(entry) = browser.entries.iter().find(|e| e.name == *file_name) {
|
||||
ui.text(&format!("Size: {}", format_size(entry.size)));
|
||||
if let Some(ext) = file_name.rsplit('.').next() {
|
||||
if file_name.contains('.') {
|
||||
ui.text(&format!("Type: .{}", ext));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `selected_file` needs to be `pub(crate)` or accessed via a method. Also `entries` field needs to be accessible. Simplest: make `selected_file` and `entries` pub.
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
Run: `cargo test -p voltex_editor --lib asset_browser -- --nocapture`
|
||||
Expected: all tests PASS
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/voltex_editor/src/asset_browser.rs
|
||||
git commit -m "feat(editor): add asset_browser_panel UI function"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Integrate into editor_demo
|
||||
|
||||
**Files:**
|
||||
- Modify: `examples/editor_demo/src/main.rs`
|
||||
|
||||
- [ ] **Step 1: Replace Console panel with AssetBrowser**
|
||||
|
||||
Add import:
|
||||
```rust
|
||||
use voltex_editor::{..., AssetBrowser, asset_browser_panel};
|
||||
```
|
||||
|
||||
Add to AppState:
|
||||
```rust
|
||||
asset_browser: AssetBrowser,
|
||||
```
|
||||
|
||||
In `resumed`, initialize:
|
||||
```rust
|
||||
let asset_browser = AssetBrowser::new(std::env::current_dir().unwrap_or_default());
|
||||
```
|
||||
|
||||
In the panel loop, replace panel 3 (Console):
|
||||
```rust
|
||||
3 => {
|
||||
asset_browser_panel(&mut state.ui, &mut state.asset_browser, rect);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build and test**
|
||||
|
||||
Run: `cargo build -p editor_demo`
|
||||
Run: `cargo test -p voltex_editor`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add examples/editor_demo/src/main.rs
|
||||
git commit -m "feat(editor): integrate asset browser into editor_demo"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/STATUS.md`
|
||||
- Modify: `docs/DEFERRED.md`
|
||||
|
||||
- [ ] **Step 1: Update STATUS.md**
|
||||
|
||||
Add: `- voltex_editor: AssetBrowser (file system browsing, directory navigation)`
|
||||
Update test count.
|
||||
|
||||
- [ ] **Step 2: Update DEFERRED.md**
|
||||
|
||||
```
|
||||
- ~~**에셋 브라우저**~~ ✅ 파일 목록 + 디렉토리 탐색 완료. 에셋 로딩/프리뷰 미구현.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/STATUS.md docs/DEFERRED.md
|
||||
git commit -m "docs: update STATUS.md and DEFERRED.md with asset browser"
|
||||
```
|
||||
Reference in New Issue
Block a user