mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
6.8 KiB
6.8 KiB
Native Drag & Drop System
A production-ready native drag-and-drop implementation for Spacedrive using AppKit on macOS.
Features
- Native OS Integration: Real
NSDraggingSession- files can be dropped into Finder, other apps - Custom Overlay: User-controlled React component follows cursor during drag
- Multi-Window Support: Drag state synchronized across all Spacedrive windows via Tauri events
- Live Updates: Overlay can react to drag events in real-time
- Type-Safe: Full TypeScript definitions matching Rust types
- File Promises: Support for virtual files generated on-drop
Quick Start
1. Start a Drag Operation
import { useDragOperation } from './hooks/useDragOperation';
function MyComponent() {
const { startDrag, isDragging } = useDragOperation({
onDragStart: (sessionId) => console.log('Started:', sessionId),
onDragEnd: (result) => console.log('Result:', result),
});
const handleDrag = async () => {
await startDrag({
items: [
{
id: 'file-1',
kind: { type: 'File', path: '/path/to/file.pdf' }
}
],
allowedOperations: ['copy', 'move'],
});
};
return <button onClick={handleDrag}>Drag File</button>;
}
2. Create a Drop Zone
import { useDropZone } from './hooks/useDropZone';
function DropTarget() {
const { isHovered, dropZoneProps } = useDropZone({
onDrop: (items) => console.log('Dropped:', items),
});
return (
<div
{...dropZoneProps}
className={isHovered ? 'border-blue-500' : 'border-gray-300'}
>
Drop files here
</div>
);
}
3. Customize the Drag Overlay
The overlay component at /drag-overlay renders during drag operations:
// apps/tauri/src/routes/DragOverlay.tsx
export function DragOverlay() {
const [session, setSession] = useState<DragSession | null>(null);
useEffect(() => {
getDragSession().then(setSession);
}, []);
return (
<div className="custom-drag-preview">
{session?.config.items.length} files
</div>
);
}
Architecture
React App Rust/Tauri macOS (Swift)
┌─────────────┐ ┌──────────────┐ ┌───────────────┐
│ │ │ │ │ │
│ startDrag() ├─invoke────► │ begin_drag ├─FFI─────► │ beginNative │
│ │ │ │ │ Drag │
│ │ │ │ │ │
│ │ │ DragCoord │ │ NSDragging │
│ │ │ inator │ │ Source │
│ │ ◄──────────┤ ◄───NSNotif──┤ │
│ onDragMoved │ emit event │ emit to all │ │ draggingMoved │
│ │ │ windows │ │ │
└─────────────┘ └──────────────┘ └───────────────┘
API Reference
useDragOperation(options)
Hook for initiating drag operations.
Options:
onDragStart?: (sessionId: string) => voidonDragMove?: (x: number, y: number) => voidonDragEnd?: (result: DragResult) => void
Returns:
isDragging: booleancurrentSession: DragSession | nullcursorPosition: { x: number; y: number } | nullstartDrag: (config) => Promise<string>cancelDrag: (sessionId) => Promise<void>
useDropZone(options)
Hook for creating drop targets.
Options:
onDrop?: (items: DragItem[]) => voidonDragEnter?: () => voidonDragLeave?: () => void
Returns:
isHovered: booleandragItems: DragItem[]dropZoneProps: object- spread onto your drop zone element
Types
type DragItemKind =
| { type: 'File'; path: string }
| { type: 'FilePromise'; name: string; mimeType: string }
| { type: 'Text'; content: string };
interface DragItem {
kind: DragItemKind;
id: string;
}
interface DragConfig {
items: DragItem[];
overlayUrl: string;
overlaySize: [number, number];
allowedOperations: DragOperation[];
}
Demo
Run the demo window:
# From apps/tauri
bun run dev
Then open the drag demo by changing the main window label to drag-demo or by programmatically showing it:
import { invoke } from '@tauri-apps/api/core';
await invoke('show_window', {
window: { type: 'Main' } // Opens the demo
});
Implementation Details
Rust Layer (src-tauri/src/drag/)
mod.rs:DragCoordinator- global state managersession.rs: Session tracking with UUIDsevents.rs: Type-safe event definitionscommands.rs: Tauri command handlers
Swift Layer (crates/macos/src-swift/drag.swift)
NativeDragSource: ImplementsNSDraggingSourceprotocol- File Promises: Implements
NSFilePromiseProviderDelegate - Notification Bridge: Sends events back to Rust via
NotificationCenter
TypeScript Layer (src/)
lib/drag.ts: Low-level API wrappershooks/useDragOperation.ts: Drag initiation hookhooks/useDropZone.ts: Drop target hookroutes/DragOverlay.tsx: Cursor-following overlay component
Known Limitations
- macOS only - Windows/Linux support not yet implemented
- Overlay mouse events: Currently the overlay ignores all mouse events (by Tauri config, not objc2 calls)
- File promise callbacks: Requires implementing file generation logic via NSNotification
Future Enhancements
- Windows Support: Implement OLE drag-drop
- Linux Support: X11/Wayland integration
- Bidirectional Drop: Handle drops FROM external apps INTO Spacedrive
- Drag Modifiers: Support Cmd/Ctrl for copy vs. move
- Multi-Monitor: Better positioning for multi-display setups
Troubleshooting
Drag doesn't start:
- Check console for Rust errors
- Ensure
begin_dragcommand is registered inmain.rs - Verify Swift build succeeded
Overlay doesn't appear:
- Check
DragOverlaywindow is created - Verify
/drag-overlayroute exists - Check for TypeScript errors in overlay component
Drop doesn't work:
- Ensure
useDropZonehook is mounted before drag starts - Check window labels match between drag source and drop target
- Verify event listeners are properly cleaned up
Contributing
This is an experimental feature. Contributions welcome, especially for:
- Windows/Linux platform support
- Better error handling
- Performance optimizations
- Additional drag item types
License
Same as Spacedrive project.