win_drag_source 0.0.2
win_drag_source: ^0.0.2 copied to clipboard
Native Windows OLE drag source for Flutter. Drag files or text with a custom RepaintBoundary ghost image, using pure Dart FFI.
win_drag_source #
English · 简体中文
Windows-only Flutter package for native OLE drag-and-drop. Wraps any widget into a drag source with a custom ghost image (no system ghost), supporting both CF_HDROP (file) and CF_UNICODETEXT (text / encrypted payload) modes.
⚠️ Windows-only. On macOS / Linux / Web the wrapper degrades to a plain
Listenerand never initiates a drag.
Features #
- ✅ Pure Dart FFI — no native code to compile, no plugin registry headache
- ✅ Custom ghost image via
RepaintBoundary— exclude badges, frames, captions from the drag preview viaimageKey - ✅ Two payload modes: file path or arbitrary text (typically encrypted)
- ✅ Tap / double-tap semantics with Windows-native 500 ms interval
- ✅ Zero runtime dependencies beyond
ffiandwin32
Installation #
dependencies:
win_drag_source: ^0.0.1
Then:
flutter pub get
Quick start #
1. File drag (CF_HDROP) #
The simplest case — drag a real file into Explorer, 3ds Max, SketchUp, chat apps, anything that accepts files:
import 'package:win_drag_source/win_drag_source.dart';
DragSource(
payload: FilePayload('C:\\path\\to\\file.max'),
onDropComplete: (accepted) {
debugPrint('drop ${accepted ? 'accepted' : 'rejected'}');
},
child: YourCard(),
)
2. Async payload (DB lookup, decryption, …) #
When the payload needs I/O before the drag can start, use payloadProvider:
DragSource(
payloadProvider: () async {
final path = await lookupFilePath(itemId);
if (path == null) return null;
return FilePayload(path);
},
onDropComplete: (ok) => ...,
child: YourCard(),
)
If payloadProvider returns null, the drag is silently aborted.
The pointer must remain pressed while it resolves; releasing it first also
cancels the drag. The asynchronous work itself does not block the UI.
3. Encrypted payload (CF_UNICODETEXT) #
For dragging to a target that knows how to decrypt the payload (e.g. your own renderer / IDE plugin):
DragSource(
payload: TextPayload(encrypt(yourJson)),
onDropComplete: (ok) => ...,
child: YourCard(),
)
The package does not ship an encryption helper — bring your own
(package:pointycastle, package:cryptography, …). The package's only job
is to deliver the bytes to the OLE drop target.
💡 Plain-text targets (Notepad, chat apps) will also receive the encrypted string and display it as opaque text. Only targets that know your format can decode it.
Custom ghost image #
By default the whole child is rasterized as the drag preview. To drag
only the cover image (excluding badges, frames, captions), wrap it in a
RepaintBoundary with a GlobalKey and pass the key as imageKey:
final _coverKey = GlobalKey();
@override
Widget build(BuildContext context) {
return DragSource(
payload: FilePayload(path),
imageKey: _coverKey, // ← rasterize this subtree, not the whole card
onDropComplete: (ok) => ...,
child: Stack(
children: [
RepaintBoundary(
key: _coverKey,
child: Image.file(File(path)),
),
Positioned(right: 8, bottom: 8, child: Badge()), // excluded from ghost
],
),
);
}
API reference #
DragSource #
| Param | Type | Default | Description |
|---|---|---|---|
child |
Widget |
— | The widget to wrap (default ghost image source) |
payload |
DragPayload? |
— | Synchronous payload |
payloadProvider |
Future<DragPayload?> Function()? |
— | Async resolver; takes precedence over payload |
imageKey |
GlobalKey? |
— | Sub-tree to rasterize as ghost image |
enabled |
bool |
true |
Master switch |
onTap |
VoidCallback? |
— | Single-tap handler |
onDoubleTap |
VoidCallback? |
— | Double-tap handler (delays single-tap by 500 ms) |
onDropComplete |
ValueChanged<bool>? |
— | Fired after DoDragDrop returns |
DragPayload #
| Class | OLE format | Use case |
|---|---|---|
FilePayload |
CF_HDROP |
Drop a real file. Receivers: Explorer, 3ds Max, chat apps, … |
TextPayload |
CF_UNICODETEXT |
Drop an arbitrary string. Receivers: any text field; or your own target that decodes the encrypted payload. |
DragDrop (low-level FFI) #
Rarely needed directly — exposed for advanced cases (custom ghost-image pipelines, etc.). Public surface:
| Method | Description |
|---|---|
createDragWindow(width, height) |
Create a topmost layered window for the ghost image |
setDragWindowPixels(hwnd, w, h, pixels) |
Push BGRA pixels into the window |
moveDragWindow(hwnd, x, y) |
Move the ghost window |
showDragWindow(hwnd) |
Show the ghost window |
destroyDragWindow(hwnd) |
Tear down the ghost window |
startFileDrag(path, hwnd, offsetX, offsetY) |
Run DoDragDrop with CF_HDROP (blocking) |
startTextDrag(text, hwnd, offsetX, offsetY) |
Run DoDragDrop with CF_UNICODETEXT (blocking) |
getCursorPosition() |
Current cursor position in screen coords |
How it works #
pointer down ──► move > 4 px ──► resolve payload
│
▼
rasterize RepaintBoundary
│
▼
create Win32 layered window
│
▼
DoDragDrop (blocking, runs its own modal loop)
│
▼
destroy window
│
▼
fire onDropComplete
The package does not hide your application window during drag. If you
need that behavior (e.g. to drop on a window behind your app), call
window_manager's hide() / show() yourself around the drag.
Caveats #
- Windows-only. On other platforms
DragSourceis a no-op. - Main-thread blocking.
DoDragDropblocks the UI thread by design while its modal message loop runs.payloadProvidermay perform async I/O; keep it quick enough that the user can hold the pointer until it resolves. pixelRatio: 1.0. Ghost images are rasterized at device pixel ratio 1.0. On high-DPI displays this may look slightly soft; if you need a crisper image, fork and adjust.
Compatibility #
The package requires Flutter 3.32 or later, Dart 3.8 or later, and Windows.
It uses OLE initialization internally, so applications do not need to set up
COM before using DragSource. A process that has explicitly initialized the
UI thread with an incompatible COM apartment model cannot start an OLE drag.
License #
MIT — see LICENSE.