Skip to content

Commit 97d1067

Browse files
committed
Drag&drop: Multi-type pasteboard, terminals now accept drops
- New `native_drag.rs` builds `NSPasteboardItem`s vending `public.file-url`, `public.utf8-plain-text` (joined paths on item 0), and `NSFilenamesPboardType` (legacy, for stock wry until tauri-apps/wry#1723 ships) - Permissive op mask (Copy|Link|Generic|Move) so terminals like Warp accept the drop instead of rejecting it; macOS arbitrates the actual op via modifier keys natively - Drops `drag` + `tauri-plugin-drag` Rust deps and `@crabnebula/tauri-plugin-drag` JS dep; both drag-out paths (single-file and multi-file selection) now route through `native_drag.rs` via `start_drag_paths` / `start_selection_drag` - Removes the `mode` parameter from drag IPC end-to-end — modifier handling belongs in the OS, not at drag-start
1 parent 19f797d commit 97d1067

14 files changed

Lines changed: 439 additions & 331 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 217 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"dependencies": {
3434
"@ark-ui/svelte": "^5.21.2",
3535
"@chenglou/pretext": "^0.0.6",
36-
"@crabnebula/tauri-plugin-drag": "^2.1.0",
3736
"@leeoniya/ufuzzy": "^1.0.19",
3837
"@logtape/logtape": "^2.0.5",
3938
"@tauri-apps/api": "^2.10.1",

apps/desktop/src-tauri/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ uuid = { version = "1.19.0", features = ["v4"] }
5151
tauri-plugin-clipboard-manager = "2"
5252
notify-debouncer-full = "0.7.0"
5353
bincode2 = "2"
54-
tauri-plugin-drag = "2.1.0"
55-
drag = "2.1.0"
5654
tauri-plugin-fs = "2.4.4"
5755
alphanumeric-sort = "1.5"
5856
chrono = "0.4"

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"store:default",
3131
"clipboard-manager:default",
3232
"mcp-bridge:default",
33-
"drag:default",
3433
"fs:allow-temp-write",
3534
"fs:allow-remove",
3635
"updater:default",

apps/desktop/src-tauri/src/commands/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ The frontend has matching TypeScript types in `$lib/tauri-commands/ipc-types.ts`
6868
- **AI commands** are registered directly from `ai::manager` and `ai::suggestions` — there is no `commands/ai.rs` file.
6969
- **Platform gates.** `volumes` is macOS-only; `mtp` and `network` are macOS+Linux; `volumes_linux` is Linux-only. Individual functions also use `#[cfg]` where behaviour differs (e.g., `sync_status`).
7070
- **`delete_files` and `rename_file` accept `volume_id`.** When set to a non-root volume, `delete_files` routes to the volume-aware delete path and skips local `validate_sources` (MTP virtual paths fail `symlink_metadata`). `rename_file` passes `volume_id` through for MTP rename support; permission checks are skipped for non-root volumes.
71-
- **`start_selection_drag`** requires the main thread. It uses `app.run_on_main_thread()` plus a `std::sync::mpsc` channel to return the result synchronously.
71+
- **`start_selection_drag` and `start_drag_paths`** require the main thread. Both delegate to a shared `run_drag_on_main_thread` helper that hops via `app.run_on_main_thread()` plus a `std::sync::mpsc` channel and returns the result synchronously. Pasteboard construction lives in `crate::native_drag` (file-URL + shell-escaped text per item).
7272
- **`list_shares_with_credentials`** has `#[allow(clippy::too_many_arguments)]` because Tauri command parameters must be top-level arguments — no struct bundling.
7373
- **`set_menu_context` and Close tab (⌘W).** When the main window loses focus, `set_menu_context("other")` disables all
7474
non-App menu items — but `CLOSE_TAB_ID` is explicitly excluded. On macOS, ⌘W means "close the front window," and the

apps/desktop/src-tauri/src/commands/file_system/drag.rs

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,34 @@
33
#[cfg(target_os = "macos")]
44
use crate::file_system::get_paths_at_indices as ops_get_paths_at_indices;
55
#[cfg(target_os = "macos")]
6+
use crate::native_drag;
7+
#[cfg(target_os = "macos")]
68
use std::path::PathBuf;
79
#[cfg(target_os = "macos")]
810
use std::sync::mpsc::channel;
911
#[cfg(target_os = "macos")]
1012
use tauri::Manager;
1113

12-
/// Initiates native drag from Rust directly, looking up paths from LISTING_CACHE (macOS only).
14+
/// Begins a native drag with the given file paths. Used for single-file drags
15+
/// where the frontend has the path directly (no listing-cache lookup needed).
16+
#[cfg(target_os = "macos")]
17+
#[tauri::command]
18+
pub fn start_drag_paths(app: tauri::AppHandle, paths: Vec<String>, icon_path: String) -> Result<(), String> {
19+
let path_bufs: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
20+
if path_bufs.is_empty() {
21+
return Err("No valid files to drag".to_string());
22+
}
23+
run_drag_on_main_thread(&app, path_bufs, PathBuf::from(icon_path))
24+
}
25+
26+
/// Stub for non-macOS platforms. Returns an error since drag is not yet implemented.
27+
#[cfg(not(target_os = "macos"))]
28+
#[tauri::command]
29+
pub fn start_drag_paths(_app: tauri::AppHandle, _paths: Vec<String>, _icon_path: String) -> Result<(), String> {
30+
Err("Drag operation is not yet supported on this platform".to_string())
31+
}
32+
33+
/// Initiates native drag from Rust directly, looking up paths from `LISTING_CACHE` (macOS only).
1334
#[cfg(target_os = "macos")]
1435
#[tauri::command]
1536
pub fn start_selection_drag(
@@ -18,64 +39,15 @@ pub fn start_selection_drag(
1839
selected_indices: Vec<usize>,
1940
include_hidden: bool,
2041
has_parent: bool,
21-
mode: String,
2242
icon_path: String,
2343
) -> Result<(), String> {
24-
// Get file paths from the cached listing
2544
let paths = ops_get_paths_at_indices(&listing_id, &selected_indices, include_hidden, has_parent)?;
2645

2746
if paths.is_empty() {
2847
return Err("No valid files to drag".to_string());
2948
}
3049

31-
// Get the main window
32-
let window = app.get_webview_window("main").ok_or("Main window not found")?;
33-
34-
// Determine drag mode (Send-safe)
35-
let is_copy_mode = mode == "copy";
36-
37-
// Store icon path for use in closure (PathBuf is Send)
38-
let icon_path_buf = PathBuf::from(icon_path);
39-
40-
// Use a channel to get the result from the main thread
41-
let (tx, rx) = channel();
42-
43-
// Run on main thread (required by macOS for drag operations)
44-
// Create DragItem inside the closure since it's not Send
45-
app.run_on_main_thread(move || {
46-
// Build DragItem inside the closure (not Send due to Data variant)
47-
let item = drag::DragItem::Files(paths);
48-
49-
// Load icon from file path
50-
let icon = drag::Image::File(icon_path_buf);
51-
52-
// Create options with the drag mode
53-
let options = drag::Options {
54-
skip_animatation_on_cancel_or_failure: false,
55-
mode: if is_copy_mode {
56-
drag::DragMode::Copy
57-
} else {
58-
drag::DragMode::Move
59-
},
60-
};
61-
62-
let result = drag::start_drag(
63-
&window,
64-
item,
65-
icon,
66-
|_result, _cursor_pos| {
67-
// Callback when drag completes - we don't need to do anything here
68-
},
69-
options,
70-
);
71-
let _ = tx.send(result);
72-
})
73-
.map_err(|e| format!("Failed to run on main thread: {}", e))?;
74-
75-
// Wait for the result
76-
rx.recv()
77-
.map_err(|_| "Failed to receive drag result")?
78-
.map_err(|e| format!("Drag operation failed: {}", e))
50+
run_drag_on_main_thread(&app, paths, PathBuf::from(icon_path))
7951
}
8052

8153
/// Stub for non-macOS platforms. Returns an error since drag is not yet implemented.
@@ -87,12 +59,28 @@ pub fn start_selection_drag(
8759
_selected_indices: Vec<usize>,
8860
_include_hidden: bool,
8961
_has_parent: bool,
90-
_mode: String,
9162
_icon_path: String,
9263
) -> Result<(), String> {
9364
Err("Drag operation is not yet supported on this platform".to_string())
9465
}
9566

67+
/// Hops to the AppKit main thread, builds the drag session, and returns synchronously.
68+
/// `NSDraggingItem`s and the source class are not `Send`, so everything happens inside
69+
/// the closure; the result travels back via a one-shot channel.
70+
#[cfg(target_os = "macos")]
71+
fn run_drag_on_main_thread(app: &tauri::AppHandle, paths: Vec<PathBuf>, icon_path: PathBuf) -> Result<(), String> {
72+
let window = app.get_webview_window("main").ok_or("Main window not found")?;
73+
let (tx, rx) = channel();
74+
75+
app.run_on_main_thread(move || {
76+
let result = native_drag::start_drag(&window, paths, &icon_path);
77+
let _ = tx.send(result);
78+
})
79+
.map_err(|e| format!("Failed to run on main thread: {}", e))?;
80+
81+
rx.recv().map_err(|_| "Failed to receive drag result")?
82+
}
83+
9684
// ============================================================================
9785
// Self-drag overlay (dynamic drag image swapping)
9886
// ============================================================================

apps/desktop/src-tauri/src/drag_image_swap.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
//! ## Timing invariant
1010
//!
1111
//! `SELF_DRAG_ACTIVE` is set via IPC from the frontend and read from AppKit swizzle
12-
//! callbacks on the main thread. The `@crabnebula/tauri-plugin-drag` `startDrag()`
13-
//! resolves **before** macOS delivers `draggingEntered:`/`draggingExited:` events.
14-
//! Therefore, state must **never** be cleared from JS async callbacks that run after
15-
//! `startDrag` resolves — they race with the swizzle. State is only cleared on drop
16-
//! (via the Tauri `clear_self_drag_overlay` command from the frontend drop handler).
12+
//! callbacks on the main thread. The `start_drag_paths` / `start_selection_drag`
13+
//! commands return **before** macOS delivers `draggingEntered:`/`draggingExited:`
14+
//! events. Therefore, state must **never** be cleared from JS async callbacks that
15+
//! run after the start call resolves — they race with the swizzle. State is only
16+
//! cleared on drop (via the Tauri `clear_self_drag_overlay` command from the
17+
//! frontend drop handler).
1718
1819
use std::ffi::CString;
1920
use std::ptr::NonNull;

apps/desktop/src-tauri/src/lib.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ use env_logger as _;
2424
use mimalloc as _;
2525
//noinspection RsUnusedImport
2626
use notify as _;
27-
//noinspection RsUnusedImport
28-
// drag is used by tauri-plugin-drag for drag-and-drop support
29-
use drag as _;
3027
//noinspection ALL
3128
// smb2 crate is used in network/smb_client module (macOS + Linux)
3229
#[cfg(any(target_os = "macos", target_os = "linux"))]
@@ -97,6 +94,8 @@ mod mcp;
9794
mod menu;
9895
#[cfg(any(target_os = "macos", target_os = "linux"))]
9996
mod mtp;
97+
#[cfg(target_os = "macos")]
98+
mod native_drag;
10099
mod net;
101100
#[cfg(any(target_os = "macos", target_os = "linux"))]
102101
mod network;
@@ -199,7 +198,6 @@ pub fn run() {
199198
.plugin(tauri_plugin_store::Builder::new().build())
200199
.plugin(tauri_plugin_opener::init())
201200
.plugin(tauri_plugin_clipboard_manager::init())
202-
.plugin(tauri_plugin_drag::init())
203201
.plugin(tauri_plugin_fs::init())
204202
.plugin(tauri_plugin_process::init())
205203
.plugin(tauri_plugin_dialog::init())
@@ -734,6 +732,7 @@ pub fn run() {
734732
commands::file_system::get_listing_stats,
735733
commands::file_system::refresh_listing_index_sizes,
736734
commands::file_system::start_selection_drag,
735+
commands::file_system::start_drag_paths,
737736
commands::file_system::prepare_self_drag_overlay,
738737
commands::file_system::clear_self_drag_overlay,
739738
// Git browser (M1: detection, chip, status column)

0 commit comments

Comments
 (0)