Drag items in the Everything tree to re-parent them: drop on a folder to move inside it, on a file to move into that file's folder, or on empty space to move to the workspace root. A Cmd/Shift multi-selection moves as one batch. Open tabs, pinned files, and expanded-folder state follow the move, and name collisions are reported instead of overwriting. Implemented with pointer events (not HTML5 DnD) so it coexists with the existing Finder-drop-to-open window handler, which otherwise suppresses HTML5 drag events in the webview. No Rust changes -- rename_entry is the move primitive and the watcher syncs both affected directories. Selection is settled on pointer-down (mousedown model) so drag and click never act on a stale selection; inline rename and drag-move share one write path (use-move-entry). The drop target highlights as a single container block, and the ghost is a 1:1 copy of the grabbed row. See SPECs/sidebar-drag-and-drop-move-spec.md.
Showing the changes to SPECs (1 of 14 files). Show all
Unified · Split
| @@ -0,0 +1,94 @@ | ||
| 1 | +# Sidebar Drag-and-Drop Move Spec | |
| 2 | + | |
| 3 | +## Summary | |
| 4 | + | |
| 5 | +Let users move files and folders by dragging them within the sidebar's `Everything` | |
| 6 | +file tree. Dropping an item onto a folder re-parents it into that folder; dropping onto a | |
| 7 | +file targets the file's parent folder; dropping on empty tree space moves to the workspace | |
| 8 | +root. The tree stays auto-sorted — this is re-parenting only, not manual ordering. | |
| 9 | + | |
| 10 | +## Goals | |
| 11 | + | |
| 12 | +- Drag a file or folder in the `Everything` tree and drop it into another folder to move it. | |
| 13 | +- Forgiving drop targets: folder → into it, file → into its parent, empty space → root. | |
| 14 | +- Support dragging a multi-selection (Cmd/Shift-click) as one batch. | |
| 15 | +- Keep open tabs, pinned files, and expanded-folder state correct after a move. | |
| 16 | +- Reuse one write path for both inline rename and drag-move. | |
| 17 | + | |
| 18 | +## Non-Goals | |
| 19 | + | |
| 20 | +- Manual reordering / custom sort order within a folder. | |
| 21 | +- Dragging files in from / out to Finder (OS drag-drop). The existing Finder-drop-to-open | |
| 22 | + behavior is preserved untouched. | |
| 23 | +- Dragging from the `Pinned` / `Recents` sections (those rows are not drag sources). | |
| 24 | +- Spring-loaded auto-expand of collapsed folders on hover (possible follow-up). | |
| 25 | + | |
| 26 | +## Why pointer events, not HTML5 drag-and-drop | |
| 27 | + | |
| 28 | +The Tauri window enables OS-level drag-drop (default) and uses it in `lib.rs` | |
| 29 | +(`WindowEvent::DragDrop`) to open files dropped from Finder. While that handler is active, | |
| 30 | +the WKWebView suppresses HTML5 `dragstart`/`dragover`/`drop` DOM events. Disabling it | |
| 31 | +(`dragDropEnabled: false`) is the only way to make HTML5 DnD fire, but that would regress | |
| 32 | +the Finder-drop-to-open feature. So drag is implemented with raw pointer events | |
| 33 | +(`pointerdown`/`pointermove`/`pointerup`), which are unaffected by the OS drag-drop config. | |
| 34 | + | |
| 35 | +## Interaction | |
| 36 | + | |
| 37 | +1. `pointerdown` settles selection up front (the mousedown-selection model): Shift/Cmd adjust | |
| 38 | + the selection and never drag; a plain press on a row outside the current multi-selection | |
| 39 | + discards that selection immediately. It then decides what a drag would carry — the whole | |
| 40 | + multi-selection if the pressed row belongs to it, otherwise just that row. A plain click | |
| 41 | + (no drag — drags suppress the click) opens/toggles the row. Settling selection at press | |
| 42 | + time keeps drag and click from ever acting on a stale selection. | |
| 43 | +2. After the pointer moves past a 4px threshold the drag begins: a floating 1:1 copy of the | |
| 44 | + grabbed row (same selected fill, size, indent, icon, and label) follows the cursor, plus a | |
| 45 | + small count badge when several items are dragged; the dragged rows dim in place. | |
| 46 | +3. On move, the folder under the pointer is resolved to a destination and its "container" | |
| 47 | + is highlighted with a soft background block — the destination folder row plus its visible | |
| 48 | + descendants when expanded (the whole tree for the root), measured into one rounded | |
| 49 | + rectangle rather than per-row outlines. The tree auto-scrolls near its top/bottom edge. | |
| 50 | +4. On `pointerup`, each dragged item is moved into the destination; the trailing click is | |
| 51 | + suppressed so a drag never also opens a file. The destination folder is expanded so the | |
| 52 | + moved items are visible. | |
| 53 | + | |
| 54 | +## Validation | |
| 55 | + | |
| 56 | +- A move is rejected (no highlight, no-op) when the destination is the item's current | |
| 57 | + parent, or when a folder is dropped onto itself or one of its descendants. | |
| 58 | +- Name collisions are detected with `file_exists` before moving; a colliding item is | |
| 59 | + skipped. After a batch, a single alert summarizes any skipped/failed items. | |
| 60 | + | |
| 61 | +## Implementation | |
| 62 | + | |
| 63 | +- `tree-move.ts` — pure helpers: `resolveDropDir`, `canMoveInto`, `computeMovePath`, and | |
| 64 | + `resolveDropRange` (the destination folder's contiguous visible-descendant run, used to | |
| 65 | + measure the highlight block) — unit-tested in `tests/tree-move.test.ts`. | |
| 66 | +- `use-move-entry.ts` — the single write path. `applyPathChange(entry, newPath)` renames on | |
| 67 | + disk via `rename_entry`, rewrites editor/workspace state (open files, pins, and—for | |
| 68 | + folders—expanded dirs), and refreshes both affected directories. `moveEntry(entry, destDir)` | |
| 69 | + guards and collision-checks, then calls it. Inline rename in `file-tree.tsx` and | |
| 70 | + `sidebar-navigator.tsx` now funnel through `applyPathChange`, removing the previously | |
| 71 | + duplicated rename tail. | |
| 72 | +- `use-tree-drag.ts` — pointer-drag state machine: `beginDrag(event, entries, primary)` arms a | |
| 73 | + drag for an already-decided set, then threshold, ghost positioning, row-geometry drop | |
| 74 | + hit-testing (`rowPathAtY`), auto-scroll, click suppression, batch drop, and a layout effect | |
| 75 | + that measures the drop-target container range into a `dropHighlight` rect. It owns drag only | |
| 76 | + — selection lives in `file-tree.tsx` and is settled on pointer-down, so the two no longer | |
| 77 | + coordinate through a shared ref at drag-start. | |
| 78 | +- During a drag, `body.tree-dragging` forces the grabbing cursor and sets `pointer-events: none` | |
| 79 | + on the rows (`[data-tree-path]`). That keeps WebKit from leaving rows stuck in `:hover` as the | |
| 80 | + cursor passes over them mid-drag; because the rows are then non-hit-testable, drop targeting | |
| 81 | + uses row geometry (`rowPathAtY`) rather than `elementFromPoint`. | |
| 82 | +- `drag-ghost.tsx` — the cursor-following 1:1 copy of the grabbed row (selected fill, size, | |
| 83 | + indent, icon, label) plus a multi-item count badge; `file-tree-icons.tsx` holds the shared | |
| 84 | + file/folder glyphs used by both tree rows and the ghost. | |
| 85 | +- `file-tree.tsx` / `file-tree-node.tsx` — wire the container ref, per-row `onPointerDown`, | |
| 86 | + the `isDragging` dim, and the single drop-highlight block rendered behind the rows. Drag | |
| 87 | + props are only passed by `FileTree`, so `Pinned`/`Recents` rows stay non-draggable. | |
| 88 | + | |
| 89 | +## Backend | |
| 90 | + | |
| 91 | +No Rust changes. `rename_entry` (`std::fs::rename`) is the move primitive, and the watcher | |
| 92 | +emits `fs:directory-changed` for both the source and destination directories, keeping other | |
| 93 | +windows and external state in sync. The immediate `refreshDirectory` calls make the active | |
| 94 | +window update instantly. | |